Skip to content

Commit 5a29a32

Browse files
author
AstroAir
committed
feat(demo,docs): expand end-to-end examples and align docs with current APIs
Implement a full demo/documentation refresh focused on core runtime coverage and consistency with src/index.ts exports. Demo updates (offline-first): - Add demo:all script to run demo, demo:xisf, demo:ser, demo:hips sequentially. - Rework demo/index.ts into an overview flow covering FITS, XISF, SER, HiPS, and XISF<->HiPS bridge checks. - Expand demo/ser-node.ts to cover parseSERBuffer/parseSERBlob, SER.fromBlob/fromNodeBuffer, frame readers, iterator, endiannessPolicy comparisons, and conversion matrix including imageIndex selection. - Expand demo/xisf-node.ts to cover XISF.fromBlob/fromNodeBuffer, signature policy call paths, distributed IO, direct writer API, and SER/HiPS bridge flows. - Expand demo/hips-node.ts to cover HiPS readTile/readAllsky/tileFormats, HiPSProperties parse/fromObject/validate/withCompatibilityFields/toString, and XISF bridge outputs. - Enhance web demos: XISF advanced conversion controls (signature policy + writer options), HiPS in-browser XISF bridge action, and FITS web page guidance text. Documentation updates: - Refresh README quick start/conversion sections and demo command docs to match current APIs and output conventions (demo/.out/*). - Rewrite outdated guide pages (installation, getting-started, project-structure, testing) to remove template remnants and reflect current repository layout. - Update SER/HiPS guides with parser/conversion strategy details and offline-first bridge examples. - Fix VitePress config base path for repository docs deployment and include a new documentation standards page in sidebar. - Correct API reference details (Node buffer-like signatures, BitPix/DataUnitType unions, utility return typing). - Update standards matrix file references and compression status notes to match current implementation/tests. Validation performed: - pnpm demo - pnpm demo:ser - pnpm demo:xisf - pnpm demo:hips - pnpm demo:all - pnpm typecheck - pnpm test -- -t "SER conversions|XISF/FITS conversion|hips-convert" - pnpm docs:build
1 parent 57f13d6 commit 5a29a32

21 files changed

Lines changed: 1341 additions & 671 deletions

README.md

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -38,74 +38,77 @@ import {
3838
SER,
3939
XISF,
4040
XISFWriter,
41+
parseSERBuffer,
42+
parseSERBlob,
4143
convertFitsToXisf,
4244
convertXisfToFits,
4345
convertSerToFits,
4446
convertFitsToSer,
47+
convertSerToXisf,
48+
convertXisfToSer,
4549
convertXisfToHiPS,
4650
convertHiPSToXisf,
4751
NodeFSTarget,
4852
Image,
4953
} from 'fitsjs-ng'
54+
import fs from 'node:fs'
5055

51-
// From a URL
52-
const fits = await FITS.fromURL('https://example.com/image.fits')
53-
54-
// From an ArrayBuffer
55-
const fits = FITS.fromArrayBuffer(buffer)
56-
57-
// From a File/Blob (browser)
58-
const fits = await FITS.fromBlob(file)
59-
60-
// From a Node.js Buffer
61-
const fits = FITS.fromNodeBuffer(fs.readFileSync('image.fits'))
56+
// FITS from ArrayBuffer / Blob / Node buffer-like / URL
57+
const fits = FITS.fromArrayBuffer(
58+
await fs.promises
59+
.readFile('image.fits')
60+
.then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)),
61+
)
62+
const fitsFromBlob = await FITS.fromBlob(new Blob([await fs.promises.readFile('image.fits')]))
63+
const fitsFromNodeBuffer = FITS.fromNodeBuffer(await fs.promises.readFile('image.fits'))
64+
const fitsFromUrl = await FITS.fromURL('https://example.com/image.fits')
6265

63-
// Access the primary header
66+
// Access header + image
6467
const header = fits.getHeader()
65-
console.log(header.get('BITPIX')) // e.g. -32
66-
console.log(header.get('NAXIS1')) // e.g. 1024
67-
68-
// Read image pixels
68+
console.log(header?.get('BITPIX'))
6969
const image = fits.getDataUnit() as Image
7070
const pixels = await image.getFrame(0)
7171
const [min, max] = image.getExtent(pixels)
7272

73-
// XISF from URL / buffer
74-
const xisf = await XISF.fromURL('https://example.com/image.xisf')
75-
76-
// SER from URL / buffer
77-
const ser = await SER.fromURL('https://example.com/capture.ser')
78-
79-
// Convert XISF -> FITS
80-
const fitsBytes = await convertXisfToFits(
81-
await fetch('https://example.com/image.xisf').then((r) => r.arrayBuffer()),
73+
// FITS <-> XISF
74+
const xisfBytes = await convertFitsToXisf(
75+
await fs.promises
76+
.readFile('image.fits')
77+
.then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)),
8278
)
83-
84-
// Convert FITS -> XISF (monolithic by default)
85-
const xisfBytes = await convertFitsToXisf(fitsBytes)
86-
87-
// Convert SER -> FITS / FITS -> SER
88-
const fitsFromSer = await convertSerToFits(
89-
await fetch('https://example.com/capture.ser').then((r) => r.arrayBuffer()),
90-
)
91-
const serFromFits = await convertFitsToSer(fitsFromSer)
92-
93-
// Convert XISF -> HiPS and HiPS -> XISF
94-
await convertXisfToHiPS(
95-
await fetch('https://example.com/image.xisf').then((r) => r.arrayBuffer()),
96-
{
97-
output: new NodeFSTarget('./out/xisf-hips'),
98-
title: 'XISF Survey',
99-
creatorDid: 'ivo://example/xisf',
100-
hipsOrder: 6,
101-
},
102-
)
103-
const xisfFromHips = await convertHiPSToXisf('./out/xisf-hips', {
79+
const xisf = await XISF.fromArrayBuffer(xisfBytes as ArrayBuffer)
80+
const fitsBytes = await convertXisfToFits(xisf)
81+
82+
// SER parse + conversions
83+
const serBytes = await fs.promises
84+
.readFile('capture.ser')
85+
.then((b) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength))
86+
const ser = SER.fromArrayBuffer(serBytes)
87+
const parsedSer = parseSERBuffer(serBytes)
88+
const parsedSerBlob = await parseSERBlob(new Blob([serBytes]))
89+
const fitsFromSer = await convertSerToFits(serBytes, { layout: 'cube' })
90+
const serFromFits = await convertFitsToSer(fitsFromSer, { sourceLayout: 'auto' })
91+
const xisfFromSer = await convertSerToXisf(serBytes)
92+
const serFromXisf = await convertXisfToSer(xisfFromSer as ArrayBuffer, { imageIndex: 0 })
93+
94+
// XISF <-> HiPS (offline/local target)
95+
const hipsTarget = new NodeFSTarget('./demo/.out/readme-quickstart-hips')
96+
await convertXisfToHiPS(xisfBytes as ArrayBuffer, {
97+
output: hipsTarget,
98+
title: 'XISF Survey',
99+
creatorDid: 'ivo://example/xisf',
100+
hipsOrder: 4,
101+
minOrder: 1,
102+
tileWidth: 128,
103+
formats: ['fits', 'png'],
104+
})
105+
const xisfCutout = await convertHiPSToXisf(hipsTarget, {
104106
cutout: { width: 512, height: 512, ra: 83.63, dec: 22.01, fov: 1.2 },
105107
})
106108

107-
// Write distributed XISF unit
108-
const distributed = await XISFWriter.toDistributed(xisf.unit)
109+
// XISF writer outputs
110+
const monolithic = await XISFWriter.toMonolithic(xisf.unit, { compression: 'zlib' })
111+
const distributed = await XISFWriter.toDistributed(xisf.unit, { compression: 'zlib' })
109112
// distributed.header => .xish bytes, distributed.blocks['blocks.xisb'] => .xisb bytes
110113
```
111114

@@ -359,13 +362,16 @@ pnpm test # Run tests
359362
pnpm build # Build library
360363
pnpm typecheck # Type check
361364
pnpm lint # Lint
365+
pnpm demo:all # Run all Node demos in sequence
362366
pnpm demo # FITS/XISF CLI demo
363367
pnpm demo:hips # HiPS Node demo (FITS->HiPS->FITS)
364368
pnpm demo:xisf # XISF Node demo (FITS<->XISF, monolithic/distributed)
365369
pnpm demo:ser # SER Node demo (SER<->FITS<->XISF)
366370
pnpm demo:web # Serve web demos (open /demo/web/index.html, /demo/web/hips.html, /demo/web/xisf.html)
367371
```
368372

373+
Node demo artifacts are written under `demo/.out/*`.
374+
369375
## Standards & Compatibility
370376

371377
- HiPS metadata and directory naming follow HiPS 1.0 conventions (`Norder*/Dir*/Npix*`, `Norder3/Allsky.*`, `properties`, `Moc.fits`).

demo/hips-node.ts

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import { mkdir, readdir, rm, writeFile } from 'node:fs/promises'
1010
import { join } from 'node:path'
1111
import {
1212
HiPS,
13+
HiPSProperties,
1314
NodeFSTarget,
15+
XISF,
16+
XISFWriter,
1417
convertFitsToHiPS,
1518
convertHiPSToFITS,
19+
convertHiPSToXisf,
20+
convertXisfToHiPS,
1621
createImageBytesFromArray,
1722
createImageHDU,
1823
lintHiPS,
@@ -49,6 +54,37 @@ function makeSampleFits(width: number, height: number): ArrayBuffer {
4954
return writeFITS([hdu])
5055
}
5156

57+
async function makePlainXisfForBridge(): Promise<ArrayBuffer> {
58+
const raw = new Uint8Array(4 * 4 * 2)
59+
const view = new DataView(raw.buffer)
60+
for (let i = 0; i < 16; i++) view.setUint16(i * 2, i * 257, true)
61+
return XISFWriter.toMonolithic({
62+
metadata: [{ id: 'XISF:CreatorApplication', type: 'String', value: 'hips-node bridge' }],
63+
images: [
64+
{
65+
id: 'BRIDGE_IMG',
66+
geometry: [4, 4],
67+
channelCount: 1,
68+
sampleFormat: 'UInt16',
69+
pixelStorage: 'Planar',
70+
colorSpace: 'Gray',
71+
dataBlock: {
72+
location: { type: 'attachment', position: 0, size: raw.byteLength },
73+
byteOrder: 'little',
74+
},
75+
data: raw,
76+
properties: [],
77+
tables: [],
78+
fitsKeywords: [],
79+
},
80+
],
81+
standaloneProperties: [],
82+
standaloneTables: [],
83+
version: '1.0',
84+
signature: { present: false, verified: true },
85+
})
86+
}
87+
5288
async function findFirstTile(root: string): Promise<{ order: number; ipix: number } | null> {
5389
const stack = [root]
5490
while (stack.length > 0) {
@@ -61,14 +97,25 @@ async function findFirstTile(root: string): Promise<{ order: number; ipix: numbe
6197
continue
6298
}
6399
const match = /Norder(\d+)[\\/]+Dir\d+[\\/]+Npix(\d+)\.(fits|png|jpg)$/iu.exec(full)
64-
if (match) {
65-
return { order: Number(match[1]), ipix: Number(match[2]) }
66-
}
100+
if (match) return { order: Number(match[1]), ipix: Number(match[2]) }
67101
}
68102
}
69103
return null
70104
}
71105

106+
function section(title: string): void {
107+
console.log(`\n${'═'.repeat(64)}`)
108+
console.log(` ${title}`)
109+
console.log('═'.repeat(64))
110+
}
111+
112+
function ok(name: string, details: Record<string, unknown>): void {
113+
const summary = Object.entries(details)
114+
.map(([key, value]) => `${key}=${String(value)}`)
115+
.join(', ')
116+
console.log(`[OK] ${name}: ${summary}`)
117+
}
118+
72119
async function main() {
73120
const outRoot = join(process.cwd(), 'demo', '.out', 'hips-node')
74121
await rm(outRoot, { recursive: true, force: true })
@@ -80,7 +127,7 @@ async function main() {
80127
const fits = makeSampleFits(256, 128)
81128
const target = new NodeFSTarget(outRoot)
82129

83-
console.log('\n1) FITS -> HiPS')
130+
section('1) FITS -> HiPS')
84131
const build = await convertFitsToHiPS(fits, {
85132
output: target,
86133
title: 'fitsjs-ng HiPS Demo',
@@ -93,17 +140,22 @@ async function main() {
93140
includeAllsky: true,
94141
includeIndexHtml: true,
95142
})
96-
console.log('Generated tiles:', build.generatedTiles)
97-
console.log('Orders:', `${build.minOrder}..${build.maxOrder}`)
143+
ok('build', {
144+
generatedTiles: build.generatedTiles,
145+
orderRange: `${build.minOrder}..${build.maxOrder}`,
146+
})
98147

99-
console.log('\n2) Lint HiPS')
148+
section('2) Lint HiPS')
100149
const lint = await lintHiPS(outRoot)
101-
console.log('Lint ok:', lint.ok)
150+
ok('lint', {
151+
ok: lint.ok,
152+
issues: lint.issues.length,
153+
})
102154
for (const issue of lint.issues) {
103155
console.log(`- [${issue.level}] ${issue.code}: ${issue.message}`)
104156
}
105157

106-
console.log('\n3) HiPS -> FITS (tile/map/cutout)')
158+
section('3) HiPS -> FITS (tile/map/cutout)')
107159
const firstTile = await findFirstTile(outRoot)
108160
if (!firstTile) throw new Error('No tile generated')
109161

@@ -127,13 +179,90 @@ async function main() {
127179
await writeFile(join(outRoot, 'demo-tile.fits'), new Uint8Array(tileFits))
128180
await writeFile(join(outRoot, 'demo-map.fits'), new Uint8Array(mapFits))
129181
await writeFile(join(outRoot, 'demo-cutout.fits'), new Uint8Array(cutoutFits))
130-
console.log('Wrote demo-tile.fits / demo-map.fits / demo-cutout.fits')
182+
ok('export fits', {
183+
tileBytes: tileFits.byteLength,
184+
mapBytes: mapFits.byteLength,
185+
cutoutBytes: cutoutFits.byteLength,
186+
})
131187

132-
console.log('\n4) HiPS class usage')
188+
section('4) HiPS class usage')
133189
const hips = await HiPS.open(outRoot)
134190
const props = await hips.getProperties()
135-
console.log('HiPS title:', props.get('obs_title'))
136-
console.log('HiPS formats:', (await hips.tileFormats()).join(', '))
191+
const tileFormats = await hips.tileFormats()
192+
const tileDecoded = await hips.readTile({
193+
order: firstTile.order,
194+
ipix: firstTile.ipix,
195+
format: tileFormats[0],
196+
})
197+
const allskyFits = await hips.readAllsky('fits')
198+
ok('readers', {
199+
title: props.get('obs_title') ?? 'n/a',
200+
formats: tileFormats.join('|'),
201+
tileShape: `${tileDecoded.width}x${tileDecoded.height}x${tileDecoded.depth}`,
202+
allskyBytes: allskyFits.byteLength,
203+
})
204+
205+
section('5) HiPSProperties API')
206+
const propsText = await target.readText('properties')
207+
const parsedProps = HiPSProperties.parse(propsText)
208+
const report = parsedProps.validate()
209+
const compatText = parsedProps.withCompatibilityFields().toString()
210+
const fromObject = HiPSProperties.fromObject({
211+
creator_did: 'ivo://fitsjs-ng/demo/object',
212+
obs_title: 'fromObject demo',
213+
dataproduct_type: 'image',
214+
hips_version: '1.4',
215+
hips_frame: 'equatorial',
216+
hips_order: 2,
217+
hips_tile_width: 64,
218+
hips_tile_format: 'fits png',
219+
})
220+
const fromObjectReport = fromObject.validate()
221+
ok('properties methods', {
222+
parsedKeys: parsedProps.keys().length,
223+
validateOk: report.ok,
224+
compatLines: compatText.split('\n').filter(Boolean).length,
225+
fromObjectOk: fromObjectReport.ok,
226+
})
227+
228+
section('6) XISF Bridge (XISF -> HiPS -> XISF cutout/map)')
229+
const bridgeRoot = join(outRoot, 'xisf-bridge')
230+
await rm(bridgeRoot, { recursive: true, force: true })
231+
await mkdir(bridgeRoot, { recursive: true })
232+
const xisfInput = await makePlainXisfForBridge()
233+
234+
await convertXisfToHiPS(xisfInput, {
235+
output: new NodeFSTarget(bridgeRoot),
236+
title: 'fitsjs-ng XISF bridge',
237+
creatorDid: 'ivo://fitsjs-ng/demo/xisf-bridge',
238+
hipsOrder: 2,
239+
minOrder: 1,
240+
tileWidth: 64,
241+
formats: ['fits', 'png'],
242+
includeAllsky: true,
243+
includeMoc: true,
244+
})
245+
const cutoutXisf = await convertHiPSToXisf(bridgeRoot, {
246+
cutout: { width: 128, height: 64, ra: 0, dec: 0, fov: 1.5 },
247+
})
248+
const mapXisf = await convertHiPSToXisf(bridgeRoot, {
249+
map: { order: 1, ordering: 'NESTED' },
250+
})
251+
await writeFile(join(outRoot, 'bridge-cutout.xisf'), new Uint8Array(cutoutXisf as ArrayBuffer))
252+
await writeFile(join(outRoot, 'bridge-map.xisf'), new Uint8Array(mapXisf as ArrayBuffer))
253+
254+
const cutoutParsed = await XISF.fromArrayBuffer(cutoutXisf as ArrayBuffer)
255+
const mapParsed = await XISF.fromArrayBuffer(mapXisf as ArrayBuffer)
256+
ok('bridge outputs', {
257+
cutoutImages: cutoutParsed.unit.images.length,
258+
cutoutGeometry: cutoutParsed.unit.images[0]?.geometry.join('x') ?? 'n/a',
259+
mapImages: mapParsed.unit.images.length,
260+
})
261+
262+
section('7) Remote backend note')
263+
console.log(
264+
'Optional remote cutout: pass backend="auto|remote" with hipsId in convertHiPSToFITS. This demo remains offline by default.',
265+
)
137266

138267
console.log('\nDone.')
139268
}

0 commit comments

Comments
 (0)