mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge origin/main: Panoramax, bikeshare Norway/Detroit, cloud filter strength, Overpass cache
Conflicts:
- KartaView viewer moved into the shared photoViewer.js on main; its
labels now take the provider name ("{name} street-level photo") and
are translated there.
- Weather: main's cloud filter strength slider, with its labels in t().
- Street Level caption: translated creator line plus main's licence.
Panoramax status and error texts are marked like KartaView's, and
"Panoramax answered HTTP {status}" gets a template in the panel and the
layer row. Catalogs: es/ru 2553 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
7fd1dd867f
77 files changed
+9201
-919
No files matched your search
@@ -13,6 +13,8 @@ import { msg } from '../../i18n/index.js';
|
||||
import { MESH_SAMPLE_RADIUS_M } from './meshSampler.js';
|
||||
import { tileBounds, tilesForBbox } from './tileMath.js';
|
||||
|
||||
/** A tile's shared point cap must move by this much to redraw it. */
|
||||
const SHARE_HYSTERESIS = 0.1;
|
||||
/** Draped lines stay at most this long while a tile's cast lines build. */
|
||||
export const SWAP_MAX_WAIT_MS = 4000;
|
||||
/** Tiles touched by new mesh samples are redrawn at most this often. */
|
||||
@@ -73,6 +75,8 @@ export function meshBoxInRange(box, centre) {
|
||||
* @property {number} viewRangeMinM Street-zoom coverage reaches at least this far…
|
||||
* @property {number} viewRangePerHeight …or camera height times this.
|
||||
* @property {number} viewNearM Ground always covered around a tilted camera.
|
||||
* @property {boolean} [keepOrder] Cap a tile's lines in the order decoded (default: newest first).
|
||||
* @property {number} [maxPoints] Line points drawn at once, over every tile (default: no limit).
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -95,6 +99,7 @@ export function meshBoxInRange(box, centre) {
|
||||
* @param {LineCoverageTuning} options.tuning
|
||||
* @param {{keyRejected?: string, rateLimited: string, tooHigh?: string}} options.messages
|
||||
* @param {(sequence: object, filter: object) => boolean} [options.passes] Whether the imagery filter keeps a line or point.
|
||||
* @param {(point: object) => number} [options.overviewPointSize] An overview point's size in pixels (default `tuning.overviewPointPx`).
|
||||
*/
|
||||
export function createLineCoverage({
|
||||
state,
|
||||
@@ -106,9 +111,13 @@ export function createLineCoverage({
|
||||
tuning,
|
||||
messages,
|
||||
passes = passesImageryFilter,
|
||||
overviewPointSize = null,
|
||||
}) {
|
||||
const { render } = state.services;
|
||||
const perTileSequenceCap = Math.floor(tuning.maxSequences / tuning.maxTiles);
|
||||
const perTilePointCap = tuning.maxPoints
|
||||
? Math.floor(tuning.maxPoints / tuning.maxTiles)
|
||||
: Infinity;
|
||||
|
||||
/** Colour for a line: the provider's, GEV cyan while selected. */
|
||||
function sequenceColor({ selected = false } = {}) {
|
||||
@@ -144,18 +153,114 @@ export function createLineCoverage({
|
||||
|
||||
/**
|
||||
* A tile's sequences, filtered then capped: capping first could leave a
|
||||
* dense tile of newer flat captures with no 360° lines at all.
|
||||
* dense tile of newer flat captures with no 360° lines at all. `measure`
|
||||
* only records what the lines need, for a draw that follows.
|
||||
*/
|
||||
function drawnSequences(entry) {
|
||||
function drawnSequences(entry, { measure = false } = {}) {
|
||||
const drawn = [];
|
||||
const current = filter();
|
||||
const selectedId = state.sequence.selectedId ?? null;
|
||||
const cap = entry.pointCap ?? perTilePointCap;
|
||||
let points = 0;
|
||||
let demand = 0;
|
||||
for (const sequence of entry.sequenceList) {
|
||||
if (drawn.length >= perTileSequenceCap) break;
|
||||
if (passes(sequence, current)) drawn.push(sequence);
|
||||
if (!passes(sequence, current)) continue;
|
||||
const size = pointsOf(sequence);
|
||||
demand += size;
|
||||
// The selected sequence is always drawn: its highlight is the answer
|
||||
// to a click, whatever the caps leave out. It is outside the cap, so
|
||||
// it is not counted against it.
|
||||
if (sequence.id === selectedId) {
|
||||
drawn.push(sequence);
|
||||
continue;
|
||||
}
|
||||
if (drawn.length >= perTileSequenceCap) continue;
|
||||
// A line past the point budget is skipped; a shorter one may still fit.
|
||||
if (points + size > cap) continue;
|
||||
points += size;
|
||||
drawn.push(sequence);
|
||||
}
|
||||
// What every passing line would take: the budget is shared by it.
|
||||
entry.pointDemand = demand;
|
||||
if (!measure) {
|
||||
entry.pointsDrawn = points;
|
||||
// The cap this draw used: a share is judged against it, not against
|
||||
// later shares that redrew nothing (small steps add up).
|
||||
entry.drawnCap = cap;
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the point budget across the tiles in view (water-filling): a
|
||||
* sparse tile keeps what it needs and the rest goes to the dense ones,
|
||||
* so a city centre is not drawn thinner than its suburbs. Sets each
|
||||
* tile's cap; returns the tiles whose cap changes what they draw.
|
||||
*/
|
||||
function shareCaps() {
|
||||
const changed = [];
|
||||
if (!tuning.maxPoints) return changed;
|
||||
const entries = [...state.coverage.tiles.values()].filter(
|
||||
(entry) => entry.kind === 'sequence',
|
||||
);
|
||||
const byDemand = [...entries].sort(
|
||||
(a, b) => (a.pointDemand || 0) - (b.pointDemand || 0),
|
||||
);
|
||||
let left = tuning.maxPoints;
|
||||
byDemand.forEach((entry, i) => {
|
||||
const share = left / (byDemand.length - i);
|
||||
const cap = Math.floor(Math.min(entry.pointDemand || 0, share));
|
||||
left -= cap;
|
||||
const before = entry.drawnCap ?? perTilePointCap;
|
||||
entry.pointCap = cap;
|
||||
// Only a change that alters what is drawn, by enough to be worth a
|
||||
// rebuild: more room for a tile that had lines left out, or well
|
||||
// under what it draws now (a small overshoot stays until the next
|
||||
// natural redraw).
|
||||
const grows =
|
||||
cap > before * (1 + SHARE_HYSTERESIS) &&
|
||||
entry.pointsDrawn < entry.pointDemand;
|
||||
const shrinks = cap < entry.pointsDrawn * (1 - SHARE_HYSTERESIS);
|
||||
if (grows || shrinks) changed.push(entry);
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Once loading settles: redraw the tiles the shared budget changes,
|
||||
* keeping their old lines until the new ones are ready (no blink).
|
||||
*/
|
||||
function shareBudget() {
|
||||
const changed = shareCaps();
|
||||
for (const entry of changed) redrawEntry(entry, { swap: true });
|
||||
if (changed.length) {
|
||||
requestRender();
|
||||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw one tile again from its decoded lines (filter, caps or selection
|
||||
* changed). With `swap`, its old lines stay until the new ones are ready.
|
||||
*/
|
||||
function redrawEntry(entry, { swap = false } = {}) {
|
||||
cancelCast(entry);
|
||||
entry.castRequested = false;
|
||||
entry.meshParts = null;
|
||||
if (swap && entry.kind === 'sequence' && entry.primitives?.length) {
|
||||
redrawTile(entry);
|
||||
return;
|
||||
}
|
||||
detachPrimitive(entry);
|
||||
attachPrimitive(entry);
|
||||
}
|
||||
|
||||
function pointsOf(sequence) {
|
||||
let count = 0;
|
||||
for (const part of sequence.parts) count += part.length;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batched primitives for a tile's sequence parts: cast polylines where
|
||||
* terrain heights are cached, draped ground lines otherwise. Each batch
|
||||
@@ -268,7 +373,22 @@ export function createLineCoverage({
|
||||
|
||||
/** Rebuild a tile's lines; the old ones stay up until the new are ready. */
|
||||
function redrawTile(entry) {
|
||||
const previous = entry.primitives;
|
||||
let previous = entry.primitives;
|
||||
if (entry.swap) {
|
||||
// A swap still building: what is on screen is its old lines and
|
||||
// whichever new ones are ready. Those stay; the rest never showed.
|
||||
const shown = [...entry.swap.old];
|
||||
const building = [];
|
||||
for (const record of previous)
|
||||
(record.primitive.ready && !record.primitive.isDestroyed?.()
|
||||
? shown
|
||||
: building
|
||||
).push(record);
|
||||
entry.swap.stop();
|
||||
entry.swap = null;
|
||||
removePrimitives(building);
|
||||
previous = shown;
|
||||
}
|
||||
entry.primitives = [];
|
||||
attachPrimitive(entry);
|
||||
removeWhenReady(entry, previous);
|
||||
@@ -427,7 +547,7 @@ export function createLineCoverage({
|
||||
collection.add({
|
||||
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
|
||||
color: tint,
|
||||
pixelSize: tuning.overviewPointPx,
|
||||
pixelSize: overviewPointSize?.(point) ?? tuning.overviewPointPx,
|
||||
// Google 3D terrain and clouds must not hide the near side's dots;
|
||||
// the horizon cull hides the far side's, which this lets through.
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
@@ -560,10 +680,13 @@ export function createLineCoverage({
|
||||
north: bounds.north + pad,
|
||||
};
|
||||
if (kind === 'sequence') {
|
||||
// Newest first; the per-tile cap applies to what passes the filter.
|
||||
entry.sequenceList = decoded.sequences.sort(
|
||||
(a, b) => (b.capturedAt || 0) - (a.capturedAt || 0),
|
||||
);
|
||||
// Newest first, unless the provider ordered them itself; the
|
||||
// per-tile cap applies to what passes the filter.
|
||||
entry.sequenceList = tuning.keepOrder
|
||||
? decoded.sequences
|
||||
: decoded.sequences.sort(
|
||||
(a, b) => (b.capturedAt || 0) - (a.capturedAt || 0),
|
||||
);
|
||||
entry.sequences = new Map();
|
||||
entry.total = decoded.sequences.length;
|
||||
} else {
|
||||
@@ -595,7 +718,10 @@ export function createLineCoverage({
|
||||
// must not drop a newer request's entry (which is what counts as loading).
|
||||
if (state.coverage.pending.get(key) === controller) {
|
||||
state.coverage.pending.delete(key);
|
||||
if (!state.coverage.pending.size) purgeStale();
|
||||
if (!state.coverage.pending.size) {
|
||||
purgeStale();
|
||||
shareBudget();
|
||||
}
|
||||
}
|
||||
notify();
|
||||
}
|
||||
@@ -735,7 +861,10 @@ export function createLineCoverage({
|
||||
state.coverage.pending.delete(key);
|
||||
}
|
||||
for (const tile of tiles) loadTile(tile, kind);
|
||||
if (!state.coverage.pending.size) purgeStale();
|
||||
if (!state.coverage.pending.size) {
|
||||
purgeStale();
|
||||
shareBudget();
|
||||
}
|
||||
// The camera moved: cells that were out of the sampler's range may not be,
|
||||
// and lines left draped by a failed, partial or dropped cast get another try.
|
||||
if (terrainMode())
|
||||
@@ -788,13 +917,12 @@ export function createLineCoverage({
|
||||
/** Rebuild every loaded tile from its decoded cache (after a filter change). */
|
||||
function rebuild() {
|
||||
purgeStale();
|
||||
for (const entry of state.coverage.tiles.values()) {
|
||||
cancelCast(entry);
|
||||
entry.castRequested = false;
|
||||
entry.meshParts = null;
|
||||
detachPrimitive(entry);
|
||||
attachPrimitive(entry);
|
||||
}
|
||||
// What each tile's lines need under the new filter, then the shares,
|
||||
// then one redraw each.
|
||||
for (const entry of state.coverage.tiles.values())
|
||||
if (entry.kind === 'sequence') drawnSequences(entry, { measure: true });
|
||||
shareCaps();
|
||||
for (const entry of state.coverage.tiles.values()) redrawEntry(entry);
|
||||
requestRender();
|
||||
notify();
|
||||
}
|
||||
@@ -848,6 +976,17 @@ export function createLineCoverage({
|
||||
|
||||
/** Recolour one sequence, every part of it, in place (selection highlight). */
|
||||
function recolorSequence(id, selected) {
|
||||
// Selected but left out by a cap: draw it (it is always admitted).
|
||||
if (selected && (tuning.maxPoints || 0) > 0)
|
||||
for (const entry of state.coverage.tiles.values())
|
||||
if (
|
||||
entry.kind === 'sequence' &&
|
||||
!entry.sequences.has(id) &&
|
||||
entry.sequenceList.some(
|
||||
(sequence) => sequence.id === id && passes(sequence, filter()),
|
||||
)
|
||||
)
|
||||
redrawEntry(entry, { swap: true });
|
||||
for (const entry of drawnEntries()) {
|
||||
const sequence = entry.sequences.get(id);
|
||||
if (!sequence) continue;
|
||||
|
||||
Reference in new issue
Block a user