SOURCE / PINNED RELEASE
Made of little things.
Atlas map viewer
- Release
- 1d2807e80b44…
- Author-recorded commit
- 43810ed2b368…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-4757dbabc86
Archive hash verified: 8db6876d96c460ac…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { AtlasReader } from '../../packages/format/src/reader';
import { MAP_LAYERS, type MapLayer } from '../../packages/format/src/index';
import { HashtreeReader } from '../../packages/hashtree/src/reader';
import { verifyPublishedMap } from '../../packages/hashtree/src/published-map';
import type { CompositionMap, CompositionMapSource } from '../../packages/renderer/src/composition';
import type { ConsumerStatus, PreviewBridge } from './bridge';
export type CatalogEntry = ConsumerStatus['catalog'][number];
const identity = (entry: CatalogEntry) => `${entry.publisher}:${entry.offer.id}`;
const COLORS = ['#2a6588', '#b06c41', '#648747', '#875f9c'];
interface Session { entry: CatalogEntry; reader: AtlasReader; ready: boolean; opening: boolean; error?: string }
/** A source session is immutable by publisher/id/root. Display preferences never enable tools. */
export class MapComposition {
catalog: CatalogEntry[] = [];
layers: MapLayer[] = [...MAP_LAYERS];
labels = { places: false, streets: false, pois: false };
imagery = false;
private selected: string[] = [];
private userSelected = false;
private discoverySettled = false;
private appliedStyle = "";
private fitted = false;
private dead = false;
private sessions = new Map<string, Session>();
private timeoutMs = 30000;
constructor(readonly map: CompositionMap, private bridge: PreviewBridge,
private changed: () => void, private error: (message: string) => void,
private read: () => void) {
map.coverage = false;
map.setLabels(false, false, false);
map.setLayers(this.layers);
}
get active() { return this.selected.flatMap(id => this.catalog.filter(entry => identity(entry) === id)); }
get opening() { return [...this.sessions.values()].some(s => s.opening); }
get readyCount() { return [...this.sessions.values()].filter(s => s.ready).length; }
get errors() { return [...this.sessions.values()].flatMap(s => s.error ? [s.error] : []); }
get cacheHits() { return [...this.sessions.values()].reduce((sum, s) => sum + s.reader.cacheHits, 0); }
isSelected(entry: CatalogEntry) { return this.selected.includes(identity(entry)); }
state(entry: CatalogEntry) { const s = this.sessions.get(entry.key); return s?.error ? 'Could not load' : s?.ready ? 'Ready' : s?.opening ? 'Opening directory…' : 'Not selected'; }
setCatalog(status: ConsumerStatus) {
if (this.dead) return;
this.timeoutMs = status.requestTimeoutMs;
this.catalog = status.catalog.filter(e => !!e.offer.hashtree);
this.discoverySettled = status.discovery.settled;
if (!this.userSelected) {
const preferred = status.preferredMapIds.flatMap(id => this.catalog.filter(e => e.offer.id === id));
const recipe = status.defaultView?.sources.filter(s => s.enabled).flatMap(s => this.catalog.filter(e => e.offer.hashtree!.root === s.root)) ?? [];
const vectors = this.catalog.filter(e => (e.offer.manifest.tileLayers ?? MAP_LAYERS).length > 0);
const defaults = status.preferredMapIds.length ? preferred : status.defaultView ? recipe
: this.discoverySettled ? (vectors.length ? vectors : this.catalog).slice(0, 1) : [];
// Discovery is incremental. Keep collecting configured/default sources until
// an explicit selection takes authority; never let the first offer win a race.
this.selected = [...new Set(defaults.map(identity))];
}
if (status.defaultView) {
// Signed paint is optional input only: never widen read purposes, enable
// labels/imagery/tools, change geometry layers, or preload anything.
const style = JSON.stringify(status.defaultView.presentation.style);
if (style !== this.appliedStyle) { this.map.setStyle(status.defaultView.presentation.style); this.appliedStyle = style; }
}
this.reconcile();
}
select(entry: CatalogEntry, enabled: boolean) {
this.userSelected = true;
const id = identity(entry);
this.selected = this.selected.filter(value => value !== id);
if (enabled) this.selected.push(id);
this.reconcile();
}
raise(entry: CatalogEntry) {
this.userSelected = true;
const id = identity(entry), index = this.selected.indexOf(id);
if (index > 0) { [this.selected[index - 1], this.selected[index]] = [this.selected[index], this.selected[index - 1]]; this.sync(); this.changed(); }
}
setLayers(layers: MapLayer[]) { this.layers = layers; this.map.setLayers(layers); this.sync(); }
setLabels(labels: typeof this.labels) {
this.labels = labels;
for (const s of this.sessions.values()) s.reader.options.purposes = this.purposes();
this.map.setLabels(labels.streets, labels.pois, labels.places);
this.sync();
}
setImagery(enabled: boolean) { this.imagery = enabled; this.sync(); }
private purposes(): Array<'map' | 'labels'> { return Object.values(this.labels).some(Boolean) ? ['map', 'labels'] : ['map']; }
private reconcile() {
if (this.dead) return;
const active = this.active, removed: Session[] = [];
for (const [key, session] of this.sessions) {
if (!active.some(e => e.key === key)) { this.sessions.delete(key); removed.push(session); }
}
// Detach renderer sessions before disposing their readers and in-flight reads.
this.sync();
for (const s of removed) s.reader.dispose();
for (const entry of active) {
const existing = this.sessions.get(entry.key);
if (existing) {
existing.entry = entry;
if (existing.ready) {
try { verifyPublishedMap(entry.offer, existing.reader); }
catch (error) { existing.ready = false; existing.error = String(error); this.error(existing.error); }
}
continue;
}
const tree = entry.offer.hashtree!;
const source = new HashtreeReader(`htree://${tree.root}`, tree, undefined, {
memoryBytes: 16 * 1024 ** 2, requestTimeoutMs: this.timeoutMs + 1000,
fetchBlob: (hash, signal) => this.bridge.blob(hash, tree.root, signal),
});
const session: Session = { entry, reader: new AtlasReader(source, () => { if (!this.dead && this.sessions.get(entry.key) === session) this.read(); }, { purposes: this.purposes() }), ready: false, opening: false };
this.sessions.set(entry.key, session);
void this.open(session);
}
this.sync(); this.fitIfReady(); this.changed();
}
private async open(session: Session) {
if (this.dead || session.opening) return;
session.opening = true; session.error = undefined;
try {
await session.reader.open();
if (this.dead || this.sessions.get(session.entry.key) !== session) return;
verifyPublishedMap(session.entry.offer, session.reader);
session.ready = true;
this.sync();
this.fitIfReady();
} catch (error) {
if (!this.dead && this.sessions.get(session.entry.key) === session) {
session.error = `${session.entry.offer.manifest.name}: ${error instanceof Error ? error.message : String(error)}`;
this.error(session.error);
}
} finally {
session.opening = false;
if (!this.dead && this.sessions.get(session.entry.key) === session) this.changed();
}
}
private fitIfReady() {
if (!this.fitted && (this.discoverySettled || this.userSelected) && this.active.length && this.active.every(e => this.sessions.get(e.key)?.ready)) {
this.map.fit(); this.fitted = true;
}
}
private sync() {
if (this.dead) return;
const sources: CompositionMapSource[] = this.active.flatMap((entry, index) => {
const session = this.sessions.get(entry.key);
if (!session?.ready) return [];
return [{ id: entry.key, reader: session.reader, manifest: session.reader.index.manifest,
enabled: true, layers: [...this.layers], labels: Object.values(this.labels).some(Boolean),
color: COLORS[index % COLORS.length], rasterOpacity: this.imagery ? 0.7 : 0 }];
});
this.map.setSources(sources);
}
retry() {
if (this.dead) return;
for (const session of this.sessions.values()) if (!session.ready && !session.opening) void this.open(session);
// Existing renderer retry path retains camera, decoded blocks, meshes and labels.
this.map.setOffline(false);
this.changed();
}
dispose() {
if (this.dead) return;
this.map.setSources([]);
this.dead = true;
for (const session of this.sessions.values()) session.reader.dispose();
this.sessions.clear();
}
}
