@motionscript/geo 0.0.0-stage → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/dist/border/format.d.ts +73 -0
- package/dist/border/format.d.ts.map +1 -0
- package/dist/border/format.js +68 -0
- package/dist/border/format.js.map +1 -0
- package/dist/border/geo-border.d.ts +44 -0
- package/dist/border/geo-border.d.ts.map +1 -0
- package/dist/border/geo-border.js +234 -0
- package/dist/border/geo-border.js.map +1 -0
- package/dist/border/geometry.d.ts +40 -0
- package/dist/border/geometry.d.ts.map +1 -0
- package/dist/border/geometry.js +382 -0
- package/dist/border/geometry.js.map +1 -0
- package/dist/border/index.d.ts +15 -0
- package/dist/border/index.d.ts.map +1 -0
- package/dist/border/index.js +15 -0
- package/dist/border/index.js.map +1 -0
- package/dist/border/loader.d.ts +31 -0
- package/dist/border/loader.d.ts.map +1 -0
- package/dist/border/loader.js +87 -0
- package/dist/border/loader.js.map +1 -0
- package/dist/border/registry.d.ts +64 -0
- package/dist/border/registry.d.ts.map +1 -0
- package/dist/border/registry.js +152 -0
- package/dist/border/registry.js.map +1 -0
- package/dist/border/selection.d.ts +21 -0
- package/dist/border/selection.d.ts.map +1 -0
- package/dist/border/selection.js +86 -0
- package/dist/border/selection.js.map +1 -0
- package/dist/border/simplify.d.ts +12 -0
- package/dist/border/simplify.d.ts.map +1 -0
- package/dist/border/simplify.js +116 -0
- package/dist/border/simplify.js.map +1 -0
- package/dist/browser/index.js +25 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +11 -0
- package/dist/globe/atmosphere.d.ts +38 -0
- package/dist/globe/atmosphere.d.ts.map +1 -0
- package/dist/globe/atmosphere.js +91 -0
- package/dist/globe/atmosphere.js.map +1 -0
- package/dist/globe/borders.d.ts +39 -0
- package/dist/globe/borders.d.ts.map +1 -0
- package/dist/globe/borders.js +116 -0
- package/dist/globe/borders.js.map +1 -0
- package/dist/globe/data/ne-110m.d.ts +25 -0
- package/dist/globe/data/ne-110m.d.ts.map +1 -0
- package/dist/globe/data/ne-110m.js +198 -0
- package/dist/globe/data/ne-110m.js.map +1 -0
- package/dist/globe/globe-places.d.ts +140 -0
- package/dist/globe/globe-places.d.ts.map +1 -0
- package/dist/globe/globe-places.js +262 -0
- package/dist/globe/globe-places.js.map +1 -0
- package/dist/globe/globe.d.ts +193 -0
- package/dist/globe/globe.d.ts.map +1 -0
- package/dist/globe/globe.js +435 -0
- package/dist/globe/globe.js.map +1 -0
- package/dist/globe/index.d.ts +17 -0
- package/dist/globe/index.d.ts.map +1 -0
- package/dist/globe/index.js +17 -0
- package/dist/globe/index.js.map +1 -0
- package/dist/globe/places.d.ts +69 -0
- package/dist/globe/places.d.ts.map +1 -0
- package/dist/globe/places.js +94 -0
- package/dist/globe/places.js.map +1 -0
- package/dist/globe/projection.d.ts +182 -0
- package/dist/globe/projection.d.ts.map +1 -0
- package/dist/globe/projection.js +215 -0
- package/dist/globe/projection.js.map +1 -0
- package/dist/globe/world-map.d.ts +83 -0
- package/dist/globe/world-map.d.ts.map +1 -0
- package/dist/globe/world-map.js +169 -0
- package/dist/globe/world-map.js.map +1 -0
- package/dist/globe/world.d.ts +46 -0
- package/dist/globe/world.d.ts.map +1 -0
- package/dist/globe/world.js +71 -0
- package/dist/globe/world.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.d.ts +19 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +19 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +69 -3
- package/registry.json +31 -0
- package/src/border/format.ts +147 -0
- package/src/border/geo-border.ts +260 -0
- package/src/border/geometry.ts +463 -0
- package/src/border/index.ts +14 -0
- package/src/border/loader.ts +97 -0
- package/src/border/registry.ts +191 -0
- package/src/border/selection.ts +95 -0
- package/src/border/simplify.ts +110 -0
- package/src/globe/atmosphere.ts +98 -0
- package/src/globe/borders.ts +166 -0
- package/src/globe/data/ne-110m.ts +214 -0
- package/src/globe/globe-places.ts +352 -0
- package/src/globe/globe.ts +552 -0
- package/src/globe/index.ts +16 -0
- package/src/globe/places.ts +132 -0
- package/src/globe/projection.ts +261 -0
- package/src/globe/world-map.ts +227 -0
- package/src/globe/world.ts +111 -0
- package/src/index.ts +5 -0
- package/src/nodes.ts +19 -0
- package/README.md +0 -4
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an author places on a Globe — the marked points, and the arcs between
|
|
3
|
+
* them — as the panel edits them and the stored field holds them.
|
|
4
|
+
*
|
|
5
|
+
* Beside `graph-equations.ts` and shaped like it, because it is the same
|
|
6
|
+
* problem: an appearance value is `number | string | boolean`, so a *list* has
|
|
7
|
+
* to arrive as one string that a mapper reads. What lives here is the authored
|
|
8
|
+
* shape and its serialization, shared by the inspector's tiles and the node's
|
|
9
|
+
* builder; what a frame actually draws is `nodes/globe/impl/places.ts`, which
|
|
10
|
+
* resolves this into coordinates and clamps it to what the render can use.
|
|
11
|
+
*
|
|
12
|
+
* The split matters in one direction: **reading must be faithful and must not
|
|
13
|
+
* mint identity**. The panel reads on every render and the builder on every
|
|
14
|
+
* rebuild, so a random id here would be a different id every time — and two
|
|
15
|
+
* things key off it (a tile's React key, and which arc endpoint points at which
|
|
16
|
+
* marker). {@link createMarker} mints ids; the readers derive a positional one
|
|
17
|
+
* and never invent.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A marked point on the surface.
|
|
22
|
+
*
|
|
23
|
+
* `label` is **not drawn**. It names the tile in the inspector and the entries
|
|
24
|
+
* in an arc's endpoint menus, which is the whole of its job today — a label
|
|
25
|
+
* rendered against the globe is a separate problem (it has to face the camera,
|
|
26
|
+
* hide when it rotates behind the planet, and not collide with its neighbours),
|
|
27
|
+
* and storing the text now is what lets that arrive without a migration.
|
|
28
|
+
*/
|
|
29
|
+
export interface GlobeMarker {
|
|
30
|
+
/** Identity across list edits — a tween matches like with like. */
|
|
31
|
+
id: string
|
|
32
|
+
/** A name for this place. Editor-facing only; see the note above. */
|
|
33
|
+
label: string
|
|
34
|
+
/** Degrees north, −90…90. */
|
|
35
|
+
lat: number
|
|
36
|
+
/** Degrees east, −180…180. */
|
|
37
|
+
lon: number
|
|
38
|
+
/** Any colour string the fill parser takes. Empty means the palette entry. */
|
|
39
|
+
color: string
|
|
40
|
+
/** Radius as a fraction of the globe's own. */
|
|
41
|
+
size: number
|
|
42
|
+
/** 0–1. A fade, unlike {@link enabled}, which is a switch. */
|
|
43
|
+
opacity: number
|
|
44
|
+
/** Whether it draws. Off keeps the tile in place, editable, with its colour. */
|
|
45
|
+
enabled: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One end of an arc: a marker it is pinned to, or a place of its own.
|
|
50
|
+
*
|
|
51
|
+
* The pinned form is the one the tile offers first, and it is not merely
|
|
52
|
+
* convenient — an arc between two *marked* places moves when the place does,
|
|
53
|
+
* where a copied pair of coordinates silently stops matching the dot it was
|
|
54
|
+
* drawn to. That is the same reasoning a state machine's connection names its
|
|
55
|
+
* ends by state id rather than by position.
|
|
56
|
+
*
|
|
57
|
+
* The free form stays because not every end wants a dot on it, and because a
|
|
58
|
+
* list written by hand or by an agent reaches for a coordinate pair first.
|
|
59
|
+
*/
|
|
60
|
+
export type ArcEnd =
|
|
61
|
+
| { readonly kind: "marker"; readonly markerId: string }
|
|
62
|
+
| { readonly kind: "place"; readonly lat: number; readonly lon: number }
|
|
63
|
+
|
|
64
|
+
/** A great-circle path between two ends. */
|
|
65
|
+
export interface GlobeArc {
|
|
66
|
+
id: string
|
|
67
|
+
from: ArcEnd
|
|
68
|
+
to: ArcEnd
|
|
69
|
+
color: string
|
|
70
|
+
/** Tube radius as a fraction of the globe's own. */
|
|
71
|
+
width: number
|
|
72
|
+
/** How far the middle lifts off the surface, as a fraction of the radius. */
|
|
73
|
+
lift: number
|
|
74
|
+
/**
|
|
75
|
+
* How much of the path is drawn, 0–1 — a line that draws itself on.
|
|
76
|
+
*
|
|
77
|
+
* A fraction of the *path* rather than a time, so it means the same thing
|
|
78
|
+
* whatever the arc's length and is the author's to tween from wherever they
|
|
79
|
+
* like. Zero draws nothing and is not an error.
|
|
80
|
+
*/
|
|
81
|
+
progress: number
|
|
82
|
+
enabled: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The palette an uncoloured marker takes its colour from, by position.
|
|
87
|
+
*
|
|
88
|
+
* Warm-to-cool rather than a spectrum: markers are read against a dark ocean and
|
|
89
|
+
* usually mean something ranked — the first is the subject and the rest are
|
|
90
|
+
* context — so the order runs from the one that carries furthest to the one that
|
|
91
|
+
* recedes. Six, then it wraps; a seventh place that has to stand out sets its
|
|
92
|
+
* own colour.
|
|
93
|
+
*/
|
|
94
|
+
export const DEFAULT_MARKER_COLORS = [
|
|
95
|
+
"#ff3b30",
|
|
96
|
+
"#ff9f0a",
|
|
97
|
+
"#ffd60a",
|
|
98
|
+
"#30d158",
|
|
99
|
+
"#5ac8fa",
|
|
100
|
+
"#bf5af2",
|
|
101
|
+
] as const
|
|
102
|
+
|
|
103
|
+
/** The colour a marker at `index` is drawn in when it names none. */
|
|
104
|
+
export function markerColorAt(index: number): string {
|
|
105
|
+
const palette = DEFAULT_MARKER_COLORS
|
|
106
|
+
return palette[((index % palette.length) + palette.length) % palette.length]!
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The colour an arc falls back to. */
|
|
110
|
+
export const DEFAULT_ARC_COLOR = "#ffd60a"
|
|
111
|
+
|
|
112
|
+
const DEFAULT_MARKER_SIZE = 0.02
|
|
113
|
+
const DEFAULT_ARC_WIDTH = 0.004
|
|
114
|
+
const DEFAULT_ARC_LIFT = 0.25
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A fresh id for a tile.
|
|
118
|
+
*
|
|
119
|
+
* A counter plus a short random suffix rather than `crypto.randomUUID`, exactly
|
|
120
|
+
* as `graph-equations.ts` explains: this package loads in a bare Node process
|
|
121
|
+
* with no DOM and no assumed globals, and an id only has to be unique within one
|
|
122
|
+
* node's list. The suffix is what stops two clients editing the same scene
|
|
123
|
+
* colliding on `mk-3`.
|
|
124
|
+
*
|
|
125
|
+
* Never called during a build — only when an author adds a tile.
|
|
126
|
+
*/
|
|
127
|
+
let placeSeq = 0
|
|
128
|
+
function nextId(prefix: string): string {
|
|
129
|
+
placeSeq += 1
|
|
130
|
+
return `${prefix}-${placeSeq}-${Math.random().toString(36).slice(2, 8)}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A new marker tile: showing, opaque, and at the given place. */
|
|
134
|
+
export function createMarker(
|
|
135
|
+
at: { lat: number; lon: number } = { lat: 0, lon: 0 }
|
|
136
|
+
): GlobeMarker {
|
|
137
|
+
return {
|
|
138
|
+
id: nextId("mk"),
|
|
139
|
+
label: "",
|
|
140
|
+
lat: at.lat,
|
|
141
|
+
lon: at.lon,
|
|
142
|
+
color: "",
|
|
143
|
+
size: DEFAULT_MARKER_SIZE,
|
|
144
|
+
opacity: 1,
|
|
145
|
+
enabled: true,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A new arc tile, between two ends. */
|
|
150
|
+
export function createArc(from: ArcEnd, to: ArcEnd): GlobeArc {
|
|
151
|
+
return {
|
|
152
|
+
id: nextId("arc"),
|
|
153
|
+
from,
|
|
154
|
+
to,
|
|
155
|
+
color: DEFAULT_ARC_COLOR,
|
|
156
|
+
width: DEFAULT_ARC_WIDTH,
|
|
157
|
+
lift: DEFAULT_ARC_LIFT,
|
|
158
|
+
progress: 1,
|
|
159
|
+
enabled: true,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** An end pinned to a marker. */
|
|
164
|
+
export function markerEnd(markerId: string): ArcEnd {
|
|
165
|
+
return { kind: "marker", markerId }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** An end at a place of its own. */
|
|
169
|
+
export function placeEnd(lat: number, lon: number): ArcEnd {
|
|
170
|
+
return { kind: "place", lat, lon }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Reading ---------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Reads a stored marker list, defensively.
|
|
177
|
+
*
|
|
178
|
+
* Never throws, and drops only what it genuinely cannot place: the field is
|
|
179
|
+
* edited a keystroke at a time by hand as well as through the tiles, so it is
|
|
180
|
+
* invalid far more often than it is valid, and a reader that threw would blank
|
|
181
|
+
* the canvas on the way from `[` to `[{`.
|
|
182
|
+
*
|
|
183
|
+
* A row with **no position at all** is the one thing that is dropped rather than
|
|
184
|
+
* defaulted. Every other field has an honest default; a latitude does not, and
|
|
185
|
+
* putting an unplaced marker on the equator would draw a dot somewhere nobody
|
|
186
|
+
* asked for and call it the author's.
|
|
187
|
+
*/
|
|
188
|
+
export function markersOf(value: unknown): GlobeMarker[] {
|
|
189
|
+
return rowsOf(value).flatMap((row, index) => {
|
|
190
|
+
const lat = finite(row.lat ?? row.latitude)
|
|
191
|
+
const lon = finite(row.lon ?? row.lng ?? row.longitude)
|
|
192
|
+
if (lat === null || lon === null) return []
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
id: idOf(row.id, `mk:${index}`),
|
|
196
|
+
label: text(row.label ?? row.name) ?? "",
|
|
197
|
+
lat: clamp(lat, -90, 90),
|
|
198
|
+
lon: wrapLongitude(lon),
|
|
199
|
+
color: text(row.color) ?? "",
|
|
200
|
+
size: positive(row.size, DEFAULT_MARKER_SIZE),
|
|
201
|
+
opacity: unit(row.opacity),
|
|
202
|
+
// Absent means on, as everywhere else a switch is stored: only an
|
|
203
|
+
// explicit `false` turns a tile off.
|
|
204
|
+
enabled: row.enabled !== false,
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Reads a stored arc list, on the same terms as {@link markersOf}. */
|
|
211
|
+
export function arcsOf(value: unknown): GlobeArc[] {
|
|
212
|
+
return rowsOf(value).flatMap((row, index) => {
|
|
213
|
+
const from = endOf(row.from)
|
|
214
|
+
const to = endOf(row.to)
|
|
215
|
+
if (!from || !to) return []
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
id: idOf(row.id, `arc:${index}`),
|
|
219
|
+
from,
|
|
220
|
+
to,
|
|
221
|
+
color: text(row.color) ?? DEFAULT_ARC_COLOR,
|
|
222
|
+
width: positive(row.width, DEFAULT_ARC_WIDTH),
|
|
223
|
+
lift: clamp(finite(row.lift) ?? DEFAULT_ARC_LIFT, 0, 4),
|
|
224
|
+
progress: unit(row.progress),
|
|
225
|
+
enabled: row.enabled !== false,
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Serializes a marker list into the stored string form. */
|
|
232
|
+
export function markersToValue(markers: readonly GlobeMarker[]): string {
|
|
233
|
+
return JSON.stringify(markers)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Serializes an arc list, writing each end back in the shape it was authored in.
|
|
238
|
+
*
|
|
239
|
+
* A pinned end is a bare **string** and a free one a `[lat, lon]` **pair**,
|
|
240
|
+
* rather than the tagged union this module works in. Two reasons, and the second
|
|
241
|
+
* is the load-bearing one: those are the two shapes somebody writing this list
|
|
242
|
+
* by hand reaches for, and a stored document is read by whatever version opens
|
|
243
|
+
* it later — so the wire form is public API and a discriminant that only exists
|
|
244
|
+
* to make the TypeScript convenient does not belong in it.
|
|
245
|
+
*/
|
|
246
|
+
export function arcsToValue(arcs: readonly GlobeArc[]): string {
|
|
247
|
+
return JSON.stringify(
|
|
248
|
+
arcs.map((arc) => ({ ...arc, from: endToValue(arc.from), to: endToValue(arc.to) }))
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function endToValue(end: ArcEnd): string | [number, number] {
|
|
253
|
+
return end.kind === "marker" ? end.markerId : [end.lat, end.lon]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* One end, from any of the three shapes a stored list holds it in.
|
|
258
|
+
*
|
|
259
|
+
* A **string** is a marker id; a **pair** is `[lat, lon]`, which is how a
|
|
260
|
+
* coordinate is written everywhere outside a program; an **object** with `lat`
|
|
261
|
+
* and `lon` is the long form. Refusing any of them would make the field's most
|
|
262
|
+
* obvious content its most surprising failure.
|
|
263
|
+
*/
|
|
264
|
+
function endOf(value: unknown): ArcEnd | null {
|
|
265
|
+
if (typeof value === "string") {
|
|
266
|
+
return value.trim() === "" ? null : markerEnd(value)
|
|
267
|
+
}
|
|
268
|
+
if (Array.isArray(value)) {
|
|
269
|
+
const lat = finite(value[0])
|
|
270
|
+
const lon = finite(value[1])
|
|
271
|
+
return lat === null || lon === null
|
|
272
|
+
? null
|
|
273
|
+
: placeEnd(clamp(lat, -90, 90), wrapLongitude(lon))
|
|
274
|
+
}
|
|
275
|
+
if (typeof value !== "object" || value === null) return null
|
|
276
|
+
const row = value as Record<string, unknown>
|
|
277
|
+
if (typeof row.markerId === "string") return markerEnd(row.markerId)
|
|
278
|
+
const lat = finite(row.lat ?? row.latitude)
|
|
279
|
+
const lon = finite(row.lon ?? row.lng ?? row.longitude)
|
|
280
|
+
return lat === null || lon === null
|
|
281
|
+
? null
|
|
282
|
+
: placeEnd(clamp(lat, -90, 90), wrapLongitude(lon))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Reading helpers -------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
type Row = Record<string, unknown>
|
|
288
|
+
|
|
289
|
+
function rowsOf(value: unknown): Row[] {
|
|
290
|
+
if (typeof value !== "string" || value.trim() === "") return []
|
|
291
|
+
try {
|
|
292
|
+
const parsed: unknown = JSON.parse(value)
|
|
293
|
+
if (!Array.isArray(parsed)) return []
|
|
294
|
+
return parsed.filter(
|
|
295
|
+
(row): row is Row => typeof row === "object" && row !== null
|
|
296
|
+
)
|
|
297
|
+
} catch {
|
|
298
|
+
return []
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A row's identity, or one derived from where it sits.
|
|
304
|
+
*
|
|
305
|
+
* Positional rather than random, and the difference is the whole point: a random
|
|
306
|
+
* id here would be a different one on every read, and both the tile's React key
|
|
307
|
+
* and an arc's pin to a marker would change under the author's hands.
|
|
308
|
+
*/
|
|
309
|
+
function idOf(value: unknown, positional: string): string {
|
|
310
|
+
return text(value) ?? positional
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function text(value: unknown): string | null {
|
|
314
|
+
return typeof value === "string" && value.trim() !== "" ? value : null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function finite(value: unknown): number | null {
|
|
318
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** A stored 0–1 value; anything unreadable is fully on. */
|
|
322
|
+
function unit(value: unknown): number {
|
|
323
|
+
const n = finite(value)
|
|
324
|
+
return n === null ? 1 : clamp(n, 0, 1)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A stored size, which must be above zero to draw at all. */
|
|
328
|
+
function positive(value: unknown, fallback: number): number {
|
|
329
|
+
const n = finite(value)
|
|
330
|
+
return n === null || n <= 0 ? fallback : n
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A longitude into −180…180.
|
|
335
|
+
*
|
|
336
|
+
* Wrapped rather than clamped, unlike latitude: 190°E is a real place (170°W)
|
|
337
|
+
* and clamping would put it on the antimeridian, where there is nothing north of
|
|
338
|
+
* the north pole for a latitude to mean.
|
|
339
|
+
*
|
|
340
|
+
* A value already in range is returned untouched — the modulo round-trip is not
|
|
341
|
+
* exact in binary floating point (139.7 comes back as 139.70000000000005), and a
|
|
342
|
+
* stored longitude that differs from the one the author typed shows up later as
|
|
343
|
+
* a diff nobody made.
|
|
344
|
+
*/
|
|
345
|
+
function wrapLongitude(value: number): number {
|
|
346
|
+
if (value >= -180 && value <= 180) return value
|
|
347
|
+
return ((((value + 180) % 360) + 360) % 360) - 180
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function clamp(value: number, min: number, max: number): number {
|
|
351
|
+
return value < min ? min : value > max ? max : value
|
|
352
|
+
}
|