mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Panoramax, the open federated street-level commons, registers beside Mapillary, Google Street View and KartaView. Its meta-catalogue (api.panoramax.xyz) is keyless and CORS-open, so it runs in the browser: - coverage from its vector tiles: sequence lines at z11–14 below 30 km (longest first under the per-tile cap), and above that the grid layer's density cells as binned points, not counted as sequences; - a click on a line opens that sequence's picture nearest the click (STAC search, any direction), without moving the camera, then any sequence's if it has none within 50 m; the 360° and since filters go to the lines and to the API; - cones from the z15 tiles' pictures layer around the open picture (~110 bytes a picture, against ~7.5 kB for a STAC item), re-centred when a picture opens outside them; ‹ › follow the picture's prev/next links; - the caption names the author (not an account named by an email address) and the picture's licence (StreetPose `license`), and links to the picture in the Panoramax viewer; the globe credits Panoramax. KartaView's viewer becomes the shared photoViewer.js (flat <img>, 360° on panorama.js, ‹ ›, keyboard, pinch, context loss), driven by a per-provider PhotoSources table; the request limiter moves to limiter.js. Panoramax shows the 2,048 px SD derivative and fetches the HD original only when the viewer is wide enough, once, shrunk while it decodes (never above 8,192 px). Orange #ff6f00 (Panoramax's own), off by default, share option 0.m.0_0.r.panoramax-1. The MCP panel allows the meta-catalogue and the biggest instances. Tests: policy, decode, source, viewer, provider, and a Panoramax fixture in the hermetic browser gate. Co-Authored-By: Claude Opus 5.5 <[email protected]>
128 lines
4.0 KiB
JavaScript
128 lines
4.0 KiB
JavaScript
import { PbfWriter } from 'pbf';
|
|
|
|
export const zigzag = (value) => (value << 1) ^ (value >> 31);
|
|
|
|
/** Tile-local [x, y] (0..extent) of a lon/lat inside tile (x, y, z). */
|
|
export function lonLatToTileLocal([lon, lat], { x, y, z }, extent = 4096) {
|
|
const n = 2 ** z;
|
|
const rad = (lat * Math.PI) / 180;
|
|
const tx = ((lon + 180) / 360) * n;
|
|
const ty =
|
|
((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n;
|
|
return [Math.round((tx - x) * extent), Math.round((ty - y) * extent)];
|
|
}
|
|
|
|
/** One MVT layer writer with deduplicated keys and appended values. */
|
|
export function layerWriter(name, extent) {
|
|
const keys = new Map();
|
|
const values = [];
|
|
const features = [];
|
|
function tags(properties) {
|
|
const out = [];
|
|
for (const [key, value] of Object.entries(properties)) {
|
|
if (value === undefined || value === null) continue;
|
|
if (!keys.has(key)) keys.set(key, keys.size);
|
|
out.push(keys.get(key), values.length);
|
|
values.push(value);
|
|
}
|
|
return out;
|
|
}
|
|
return {
|
|
add(type, geometry, properties, id) {
|
|
features.push({ type, geometry, tags: tags(properties), id });
|
|
},
|
|
write(pbf) {
|
|
pbf.writeVarintField(15, 2);
|
|
pbf.writeStringField(1, name);
|
|
pbf.writeVarintField(5, extent);
|
|
for (const feature of features)
|
|
pbf.writeMessage(
|
|
2,
|
|
(f, out) => {
|
|
if (Number.isInteger(f.id)) out.writeVarintField(1, f.id);
|
|
out.writePackedVarint(2, f.tags);
|
|
out.writeVarintField(3, f.type);
|
|
out.writePackedVarint(4, f.geometry);
|
|
},
|
|
feature,
|
|
);
|
|
for (const key of keys.keys()) pbf.writeStringField(3, key);
|
|
for (const value of values)
|
|
pbf.writeMessage(
|
|
4,
|
|
(v, out) => {
|
|
if (typeof v === 'boolean') out.writeBooleanField(7, v);
|
|
else if (typeof v === 'number') out.writeVarintField(5, v);
|
|
else out.writeStringField(1, String(v));
|
|
},
|
|
value,
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
/** MVT MoveTo/LineTo commands; the cursor carries over between parts. */
|
|
export function lineGeometry(parts) {
|
|
const out = [];
|
|
let cx = 0;
|
|
let cy = 0;
|
|
for (const part of parts) {
|
|
if (part.length < 2) continue;
|
|
const [x0, y0] = part[0];
|
|
out.push(9, zigzag(x0 - cx), zigzag(y0 - cy));
|
|
cx = x0;
|
|
cy = y0;
|
|
out.push(((part.length - 1) << 3) | 2);
|
|
for (const [x, y] of part.slice(1)) {
|
|
out.push(zigzag(x - cx), zigzag(y - cy));
|
|
cx = x;
|
|
cy = y;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Encode a `mly1_public`-shaped tile from lon/lat features, for tests and
|
|
* the hermetic QA gate.
|
|
* @param {{x: number, y: number, z: number}} tile
|
|
* @param {{sequences?: Array<{id: string, capturedAt?: number, isPano?: boolean, parts: Array<Array<[number, number]>>}>, overview?: Array<{id: string, lon: number, lat: number, capturedAt?: number, isPano?: boolean}>, extent?: number}} [content]
|
|
*/
|
|
export function encodeCoverageTile(
|
|
tile,
|
|
{ sequences = [], overview = [], extent = 4096 } = {},
|
|
) {
|
|
const pbf = new PbfWriter();
|
|
if (sequences.length) {
|
|
const layer = layerWriter('sequence', extent);
|
|
for (const sequence of sequences)
|
|
layer.add(
|
|
2,
|
|
lineGeometry(
|
|
sequence.parts.map((part) =>
|
|
part.map((point) => lonLatToTileLocal(point, tile, extent)),
|
|
),
|
|
),
|
|
{
|
|
id: sequence.id,
|
|
captured_at: sequence.capturedAt ?? Date.now(),
|
|
is_pano: sequence.isPano === true,
|
|
},
|
|
);
|
|
pbf.writeMessage(3, (_, out) => layer.write(out), null);
|
|
}
|
|
if (overview.length) {
|
|
const layer = layerWriter('overview', extent);
|
|
for (const point of overview) {
|
|
const [px, py] = lonLatToTileLocal([point.lon, point.lat], tile, extent);
|
|
layer.add(1, [9, zigzag(px), zigzag(py)], {
|
|
id: point.id,
|
|
captured_at: point.capturedAt ?? Date.now(),
|
|
is_pano: point.isPano === true,
|
|
});
|
|
}
|
|
pbf.writeMessage(3, (_, out) => layer.write(out), null);
|
|
}
|
|
return new Uint8Array(pbf.finish());
|
|
}
|