Files
gods-eye-view/mobile/globe/pins.js
T

197 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Pins for discourse and capture (DESIGN.md §11.3–11.4): posts, sealed
* witness captures, public camera eyes, live broadcasts and holograms. Eyes
* and live pins draw their view frustum as a translucent wedge on the ground
* along the camera heading. Pins are few (tens), so the wedge primitive is
* simply rebuilt on change.
*/
import * as Cesium from 'cesium';
import { glyph } from './glyphs.js';
import { offsetLonLat } from './geo.js';
export const PIN_KINDS = Object.freeze([
'post',
'witness',
'eye',
'live',
'hologram',
]);
const PIN_COLORS = Object.freeze({
post: '#E8EEF7',
witness: '#FFC857',
eye: '#FFD166',
live: '#FF4D6D',
hologram: '#C9A7FF',
});
const PIN_TITLES = Object.freeze({
post: 'Post',
witness: 'Sealed witness capture',
eye: 'Public camera',
live: 'Live broadcast',
hologram: 'Hologram',
});
export function createPins({ scene, requestRender }) {
const root = scene.primitives.add(new Cesium.PrimitiveCollection());
const billboards = root.add(new Cesium.BillboardCollection());
const edges = root.add(new Cesium.PolylineCollection());
let wedges = null;
const pins = new Map();
let pulse = 0;
function rebuildWedges() {
if (wedges) root.remove(wedges);
wedges = null;
edges.removeAll();
const instances = [];
for (const p of pins.values()) {
if (p.kind !== 'eye' && p.kind !== 'live') continue;
if (!Number.isFinite(p.headingDeg)) continue;
const half = (p.fovDeg ?? 60) / 2;
const ring = [[p.lon, p.lat]];
for (let a = -half; a <= half + 0.01; a += Math.max(2, half / 6))
ring.push(offsetLonLat(p.lon, p.lat, p.headingDeg + a, p.rangeM));
const css = p.color || PIN_COLORS[p.kind];
instances.push(
new Cesium.GeometryInstance({
id: { layer: 'pins', key: p.id },
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(
Cesium.Cartesian3.fromDegreesArray(ring.flat()),
),
height: (p.altM || 0) + 3,
vertexFormat: Cesium.PerInstanceColorAppearance.FLAT_VERTEX_FORMAT,
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.fromCssColorString(css).withAlpha(0.28),
),
},
}),
);
edges.add({
positions: Cesium.Cartesian3.fromDegreesArrayHeights(
[...ring, ring[0]].flatMap(([lo, la]) => [lo, la, (p.altM || 0) + 4]),
),
width: 1.5,
material: Cesium.Material.fromType('Color', {
color: Cesium.Color.fromCssColorString(css).withAlpha(0.85),
}),
});
}
if (instances.length)
wedges = root.add(
new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: true,
}),
asynchronous: true,
}),
);
}
function rebuildBillboards() {
billboards.removeAll();
for (const p of pins.values()) {
const g = glyph(p.kind);
p._bb = billboards.add({
position: Cesium.Cartesian3.fromDegrees(
p.lon,
p.lat,
(p.altM || 0) + 8,
),
image: g.image,
imageId: g.imageId,
color: Cesium.Color.fromCssColorString(p.color || PIN_COLORS[p.kind]),
scale: 0.62,
scaleByDistance: new Cesium.NearFarScalar(2e3, 1.15, 8e6, 0.55),
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: 2e5,
id: { layer: 'pins', key: p.id },
});
}
}
function changed() {
rebuildBillboards();
rebuildWedges();
requestRender();
}
return {
add(pin) {
if (!pin || pin.id == null) throw new TypeError('addPin needs an id');
if (!Number.isFinite(pin.lon) || !Number.isFinite(pin.lat))
throw new TypeError('addPin needs lon and lat');
const kind = PIN_KINDS.includes(pin.kind) ? pin.kind : 'post';
const p = {
id: String(pin.id),
lon: pin.lon,
lat: pin.lat,
kind,
headingDeg: Number.isFinite(pin.headingDeg) ? pin.headingDeg : null,
fovDeg: Number.isFinite(pin.fovDeg) ? pin.fovDeg : 60,
rangeM: Number.isFinite(pin.rangeM) ? pin.rangeM : 350,
color: pin.color || null,
label: pin.label || '',
altM: Number.isFinite(pin.altM) ? pin.altM : 0,
};
pins.set(p.id, p);
changed();
return p.id;
},
remove(id) {
const ok = pins.delete(String(id));
if (ok) changed();
return ok;
},
list() {
return [...pins.values()].map(({ _bb, ...p }) => ({ ...p }));
},
locate(id) {
const p = pins.get(String(id));
return p ? [p.lon, p.lat, p.altM || 0] : null;
},
pick(id) {
const p = pins.get(String(id));
if (!p) return null;
return {
kind: 'pin',
pinKind: p.kind,
id: p.id,
title: p.label || PIN_TITLES[p.kind],
subtitle: PIN_TITLES[p.kind],
lon: p.lon,
lat: p.lat,
meta: {
layer: 'pins',
headingDeg: p.headingDeg,
fovDeg: p.fovDeg,
label: p.label,
},
};
},
/** Live pins breathe once a second; returns true when anything changed. */
tick() {
let any = false;
pulse = (pulse + 1) % 2;
for (const p of pins.values())
if (p.kind === 'live' && p._bb) {
p._bb.scale = pulse ? 0.74 : 0.6;
any = true;
}
return any;
},
get billboards() {
return billboards;
},
destroy() {
scene.primitives.remove(root);
},
};
}