Spaces:
Configuration error
Configuration error
| // The surface model vocabulary, and every consequence of picking one. | |
| // | |
| // Kept here, free of Cesium, because three places have to agree about what a | |
| // selection implies and none of them should restate it: the url schema, the Map | |
| // menu (which has to say when a control no longer describes the picture), and | |
| // SurfaceModel (which executes it). The matrix is also the part worth testing — | |
| // see surfaceModels.test.ts. | |
| // | |
| // The two models are deliberately not symmetric. OsmBuildings adds extruded | |
| // footprints to the globe's own surface; GooglePhotorealistic *is* the surface, | |
| // ground and vegetation and buildings in one mesh, so the globe underneath has | |
| // to go. See docs/adr/0005-surface-models.md. | |
| import { SKY_MODE } from "./viewModes"; | |
| export const SURFACE_MODELS = ["None", "OsmBuildings", "GooglePhotorealistic"] as const; | |
| // Named for what it is — a name — because `SurfaceModel` is also the class that | |
| // owns the tileset (src/modules/SurfaceModel.ts), and one import trap of that | |
| // kind is enough (see the note on SceneMode in src/config/viewModes.ts). | |
| export type SurfaceModelName = (typeof SURFACE_MODELS)[number]; | |
| export type SurfaceTileset = Exclude<SurfaceModelName, "None">; | |
| /** The Map menu groups whose selection a surface model can render meaningless. */ | |
| export type MapGroup = "layers" | "terrain"; | |
| interface SurfaceModelRules { | |
| /** | |
| * The view modes the model applies in. | |
| * | |
| * GooglePhotorealistic is the sky view only, and that is a cost decision, not | |
| * a technical one: streamed from a fixed viewpoint looking up, tile loading is | |
| * bounded by where the observer stands, where on the globe it is bounded only | |
| * by how far someone cares to fly. Widening it is this one line. | |
| * | |
| * Neither applies in 2D or Columbus. Cesium does not refuse a tileset there — | |
| * `Cesium3DTile` carries a 2D screen-space-error branch — so this is a choice | |
| * too: full tile bandwidth for geometry that reads as broken is worse than | |
| * nothing. | |
| */ | |
| viewModes: readonly string[]; | |
| /** Whether the model replaces the globe's surface rather than adding to it. */ | |
| hidesGlobe: boolean; | |
| /** The terrain the model's heights assume, forced for as long as it is up. */ | |
| terrain?: string; | |
| } | |
| const RULES: Record<SurfaceModelName, SurfaceModelRules> = { | |
| None: { | |
| viewModes: ["3D", "2D", "Columbus", SKY_MODE], | |
| hidesGlobe: false, | |
| }, | |
| OsmBuildings: { | |
| viewModes: ["3D", SKY_MODE], | |
| hidesGlobe: false, | |
| // The tileset is authored against Cesium World Terrain and Cesium has no | |
| // ground-clamping for tilesets, so any other terrain leaves the buildings | |
| // floating or buried by the difference — metres in flat country, far more in | |
| // the mountains, and worst of all directly under a sky-view observer. | |
| terrain: "CesiumWorldTerrain", | |
| }, | |
| GooglePhotorealistic: { | |
| viewModes: [SKY_MODE], | |
| hidesGlobe: true, | |
| }, | |
| }; | |
| export interface SurfaceEffects { | |
| /** The tileset the scene should hold, or undefined for none. */ | |
| tileset: SurfaceTileset | undefined; | |
| hideGlobe: boolean; | |
| /** Terrain to impose over the user's choice, or undefined to honour it. */ | |
| terrain: string | undefined; | |
| /** Groups whose controls no longer describe what is drawn. */ | |
| inert: readonly MapGroup[]; | |
| /** | |
| * Models that cannot apply in this view mode. They stay selectable — arming | |
| * one before entering the sky view is a reasonable thing to do, and it is what | |
| * makes `?surface=GooglePhotorealistic&scene=Sky` a working link — so the menu | |
| * annotates them rather than disabling them. | |
| */ | |
| unavailable: readonly SurfaceModelName[]; | |
| } | |
| export function isSurfaceModelName(value: string): value is SurfaceModelName { | |
| return (SURFACE_MODELS as readonly string[]).includes(value); | |
| } | |
| /** | |
| * What a selection means in a given view mode: what to draw, what to hide, what | |
| * to override, and what the menu should stop claiming. | |
| * | |
| * Total in both arguments, and the single source for all four answers, so the | |
| * menu's annotations cannot drift from what the scene actually does. | |
| */ | |
| export function surfaceEffects(surfaceModel: string, viewMode: string): SurfaceEffects { | |
| const unavailable = SURFACE_MODELS.filter((name) => !RULES[name].viewModes.includes(viewMode)); | |
| const selected: SurfaceModelName = isSurfaceModelName(surfaceModel) ? surfaceModel : "None"; | |
| const rules = RULES[selected]; | |
| // Suppressed, never deselected: an unavailable model keeps its place in the | |
| // store and the url, so leaving 2D or entering the sky view brings it back | |
| // rather than making the user ask twice. | |
| const active = selected !== "None" && rules.viewModes.includes(viewMode); | |
| if (!active) { | |
| return { tileset: undefined, hideGlobe: false, terrain: undefined, inert: [], unavailable }; | |
| } | |
| const inert: MapGroup[] = []; | |
| // Nothing is drawn of the globe, so neither its imagery nor its terrain is. | |
| if (rules.hidesGlobe) { | |
| inert.push("layers", "terrain"); | |
| } else if (rules.terrain !== undefined) { | |
| inert.push("terrain"); | |
| } | |
| return { | |
| tileset: selected as SurfaceTileset, | |
| hideGlobe: rules.hidesGlobe, | |
| terrain: rules.terrain, | |
| inert, | |
| unavailable, | |
| }; | |
| } | |
| /** | |
| * How a view mode is said in a sentence, where that differs from its name. "Sky" | |
| * is the app's own name for a vantage point rather than a projection, and reads | |
| * as "the sky view" in prose. | |
| */ | |
| const VIEW_MODE_PROSE: Record<string, string> = { [SKY_MODE]: "sky" }; | |
| /** | |
| * The menu's explanation for a model that cannot apply in the current view mode, | |
| * built from the same `viewModes` the suppression is built from. | |
| * | |
| * Derived rather than written out, because a sentence written out is a second | |
| * place the rule lives: widening `viewModes` would leave the menu asserting a | |
| * restriction that no longer exists, which is worse than saying nothing. It also | |
| * keeps the claim in docs/adr/0005-surface-models.md true — widening really is | |
| * the one line. | |
| */ | |
| export function viewModeNote(surfaceModel: string): string { | |
| if (!isSurfaceModelName(surfaceModel)) { | |
| return ""; | |
| } | |
| const modes = RULES[surfaceModel].viewModes.map((mode) => VIEW_MODE_PROSE[mode] ?? mode); | |
| if (modes.length === 0) { | |
| return "Applies in no view mode"; | |
| } | |
| const last = modes[modes.length - 1] as string; | |
| const list = modes.length === 1 ? last : `${modes.slice(0, -1).join(", ")} and ${last}`; | |
| return `Applies in the ${list} view${modes.length === 1 ? "" : "s"} only`; | |
| } | |
| // No name accessor here, unlike the imagery and terrain registries: those exist | |
| // to *filter* what is selectable (base vs overlay, visible vs hidden), and every | |
| // surface model is selectable. `SURFACE_MODELS` is the list; a function returning | |
| // it verbatim, behind a getter returning that, was two layers of nothing. | |