Files
gods-eye-view/src/sources/openFreeMap.js
T
German KhokhlovandClaude Opus 5.5 00704ddc89 i18n: fix review findings and polish ru/es strings
Fixes the 51 P0–P2 findings from the full review of the i18n branch and
the issues found while checking every new string on screen in ru and es.

- Translate errors and statuses that still reached ru/es in English:
  feed and tile errors, provider statuses, the ALPR chip, the traffic
  mode chip, voice plan step labels, fire feed values, receiver bands,
  scene save reasons and the Cesium attribution link.
- Show preset city names in the UI language (src/ui/placeNames.js);
  data and everything sent to the model stay English.
- Share layer names between the layer panel, HUD and voice card
  (src/ui/layerNames.js).
- Keep long ru/es strings whole: voice card error lines, analyst
  titles, the voice tier status, the Nepal evidence button, the ALPR
  retry chip. English layout is unchanged.
- Ask before a language switch drops unsaved work.
- Catalog fixes: glossary terms (пуск, пресет), gender agreement and
  consistent "peticiones" in es, fewer calques in both languages.
- Fix English text that was itself wrong (e.g. "Exit cockpit to fly to
  a weather", raw tool ids in voice plan steps).

Catalogs: es/ru 2535 messages each, complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 23:49:52 +04:00

240 lines
7.6 KiB
JavaScript

import { PbfReader } from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';
import { createVectorTileSource } from './vectorTiles.js';
import { clipTileRing, militaryOutlineLines } from './militaryTileGeometry.js';
import { tileToBBox } from '../data/tomtomTiles.js';
// Kept English in data; the map card and Contacts translate it.
import { UNNAMED_MILITARY_AREA } from '../data/installationNames.js';
/** Name of a military polygon the names pack has not named. */
export { UNNAMED_MILITARY_AREA };
const ROAD_TYPES = Object.freeze({
motorway: 'motorway',
trunk: 'trunk',
primary: 'primary',
secondary: 'secondary',
tertiary: 'tertiary',
minor: 'residential',
});
/**
* Surface traffic uses public motor-road classes only. Untyped service ways
* include car parks and grounds access, so none are used as through traffic.
* OpenMapTiles folds private access into `no`. Tunnels cannot be placed on
* the surface above them; ramps/bridges remain valid only on a motor road.
*/
export function isDrivableOpenMapRoad(properties = {}) {
return (
Object.hasOwn(ROAD_TYPES, properties.class) &&
!properties.subclass &&
!properties.service &&
![
'no',
'private',
'destination',
'customers',
'delivery',
'agricultural',
'forestry',
].includes(properties.access) &&
properties.brunnel !== 'tunnel' &&
!properties.indoor
);
}
/** Translate public motor roads; reverse negative one-way geometry. */
export function openMapRoad(coordinates, properties = {}) {
const type = ROAD_TYPES[properties.class];
if (!isDrivableOpenMapRoad(properties) || coordinates.length < 2) return null;
const reverse = Number(properties.oneway) === -1;
return {
coordinates: reverse ? coordinates.slice().reverse() : coordinates,
type,
drivable: true,
roadClass: properties.class,
roadProperties: { ...properties },
oneway: reverse || Number(properties.oneway) === 1 ? 1 : 0,
ramp: properties.ramp === 1,
brunnel: properties.brunnel || null,
};
}
/** Area-weighted centroid in tile-scale longitude/latitude coordinates. */
export function polygonCentroid(ring) {
if (!ring?.length) return null;
const [ox, oy] = ring[0];
let area = 0,
x = 0,
y = 0;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const ax = ring[j][0] - ox,
ay = ring[j][1] - oy;
const bx = ring[i][0] - ox,
by = ring[i][1] - oy;
const cross = ax * by - bx * ay;
area += cross;
x += (ax + bx) * cross;
y += (ay + by) * cross;
}
return Math.abs(area) > 1e-15
? [ox + x / (3 * area), oy + y / (3 * area)]
: ring[0];
}
/** Clip a line to its tile core so buffered copies never animate duplicate dots. */
export function clipTileLine(coords, box) {
const lines = [];
let line = [];
for (let i = 1; i < coords.length; i++) {
const a = coords[i - 1],
b = coords[i];
const dx = b[0] - a[0],
dy = b[1] - a[1];
let lo = 0,
hi = 1;
const p = [-dx, dx, -dy, dy],
q = [
a[0] - box.west,
box.east - a[0],
a[1] - box.south,
box.north - a[1],
];
for (let k = 0; k < 4; k++) {
if (p[k] === 0) {
if (q[k] < 0) hi = -1;
} else if (p[k] < 0) lo = Math.max(lo, q[k] / p[k]);
else hi = Math.min(hi, q[k] / p[k]);
}
if (lo > hi) {
if (line.length > 1) lines.push(line);
line = [];
continue;
}
const start = [a[0] + lo * dx, a[1] + lo * dy],
end = [a[0] + hi * dx, a[1] + hi * dy];
const last = line.at(-1);
if (
!last ||
Math.abs(last[0] - start[0]) + Math.abs(last[1] - start[1]) > 1e-10
) {
if (line.length > 1) lines.push(line);
line = [start];
}
line.push(end);
}
if (line.length > 1) lines.push(line);
return lines.filter((points) => {
let metres = 0;
for (let i = 1; i < points.length; i++)
metres +=
Math.hypot(
(points[i][0] - points[i - 1][0]) *
Math.cos((points[i][1] * Math.PI) / 180),
points[i][1] - points[i - 1][1],
) * 111320;
return metres >= 12;
});
}
/** Decode road lines and unnamed military polygons from an OpenMapTiles tile. */
export function decodeOpenFreeMapTile(bytes, z, x, y) {
return decodeLayers(bytes, z, x, y, ['transportation', 'landuse']);
}
/** Decode only the unnamed military polygons; installations never use roads. */
export function decodeOpenFreeMapMilitaryTile(bytes, z, x, y) {
return decodeLayers(bytes, z, x, y, ['landuse']);
}
function decodeLayers(bytes, z, x, y, layerNames) {
const tile = new VectorTile(new PbfReader(bytes));
const roads = [],
military = [];
const box = tileToBBox(z, x, y);
for (const name of layerNames) {
const layer = tile.layers[name];
if (!layer) continue;
if (layer.length > 40_000)
throw new Error('Vector tile feature limit exceeded');
for (let i = 0; i < layer.length; i++) {
const feature = layer.feature(i),
props = feature.properties;
if (
name === 'transportation'
? !isDrivableOpenMapRoad(props)
: props.class !== 'military'
)
continue;
const geometry = feature.toGeoJSON(x, y, z).geometry;
if (name === 'transportation') {
const lines =
geometry.type === 'LineString'
? [geometry.coordinates]
: geometry.type === 'MultiLineString'
? geometry.coordinates
: [];
for (const coords of lines)
for (const clipped of clipTileLine(coords, box))
roads.push(openMapRoad(clipped, props));
} else {
const polygons =
geometry.type === 'Polygon'
? [geometry.coordinates]
: geometry.type === 'MultiPolygon'
? geometry.coordinates
: [];
for (let p = 0; p < polygons.length; p++) {
const rings = polygons[p]
.map((r) => clipTileRing(r, box))
.filter((r) => r.length >= 4);
const ring = rings[0],
centroid = polygonCentroid(ring);
if (!centroid || ring.length < 4) continue;
const [longitude, latitude] = centroid;
military.push({
id: `ofm:${z}/${x}/${y}:${feature.id ?? i}:${p}`,
kind: 'installation',
class: 'military_land',
name: UNNAMED_MILITARY_AREA,
latitude,
longitude,
footprint: ring,
rings,
featureKey:
feature.id == null
? `tile:${z}/${x}/${y}:${i}`
: String(feature.id),
tileEpsilon: (box.east - box.west) / feature.extent,
tileBounds: box,
tileZoom: z,
outlineLines: rings.flatMap((r) => militaryOutlineLines(r, box)),
validation: 'unreviewed',
sources: [{ name: 'OpenStreetMap', id: `tile:${z}/${x}/${y}` }],
});
}
}
}
}
return { roads, military };
}
/**
* Construct an immutable-version road/military tile source without starting I/O.
*
* `allowedOrigin` is deliberately not set here: `createVectorTileSource`
* derives it from whichever `tileJsonUrl` is in effect, so a caller that
* repoints only `tileJsonUrl` gets tiles from that host or a named error —
* never a silent fall back to the default host (#933). Passing
* `allowedOrigin` explicitly still overrides both.
*/
export function createOpenFreeMapSource(options = {}) {
return createVectorTileSource({
tileJsonUrl: 'https://tiles.openfreemap.org/planet',
decode: decodeOpenFreeMapTile,
maxEntries: 192,
maxCacheBytes: 64 * 1024 * 1024,
...options,
});
}