@rr0/ufoathome 0.12.4 → 0.12.6

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/README.md CHANGED
@@ -100,7 +100,8 @@ mechanism.
100
100
  ## `<rr0-ufo-recorder>` — full editor
101
101
 
102
102
  The authoring component (~540KB gzip — see below for why): everything `<rr0-ufo>` has, plus a shape/appearance
103
- toolbar (oval/saucer/triangle presets, color, transparency, halo) and drag-to-record. It composes a nested
103
+ toolbar (oval/polygon presets, color, transparency, halo, and the object's real reported
104
+ size/distance — see [Apparent size](#apparent-size)) and drag-to-record. It composes a nested
104
105
  `<rr0-scene>` internally — not a bare `<rr0-ufo>` — so the shape being drawn is always seen against the
105
106
  sighting's own real sky, computed live from whatever latitude/longitude/heading/orientation/observation-time
106
107
  fields the toolbar currently holds (see [Architecture](#architecture)). This absorbs `<rr0-scene>`'s own
@@ -111,8 +112,23 @@ this heavier authoring component.
111
112
 
112
113
  ```html
113
114
  <rr0-ufo-recorder></rr0-ufo-recorder>
115
+ <rr0-ufo-recorder src="sighting.json"></rr0-ufo-recorder>
114
116
  ```
115
117
 
118
+ With `src`, the editor opens on an existing recording instead of an empty canvas — the same
119
+ attribute the three other elements take. That is what makes an editor URL per observation possible:
120
+ rr0.org's own editor page maps its `?sighting=` parameter onto it, and
121
+ [ufoathome.org](https://ufoathome.org) redirects any path it is given into that parameter, so
122
+
123
+ - `https://ufoathome.org/science/crypto/ufo/enquete/dossier/Socorro/sighting.json`, or simply
124
+ - `https://ufoathome.org/Socorro` for a case dossier of that site (a value with no `/` is expanded
125
+ to `/science/crypto/ufo/enquete/dossier/<name>/sighting.json`),
126
+
127
+ opens that observation for editing. The page only accepts recordings from its own origin: a shared
128
+ link must not be able to display a recording fabricated elsewhere inside an rr0.org page. To load
129
+ one from anywhere else, use the editor's own **Load from URL** field, which is an explicit gesture
130
+ by whoever is sitting at the keyboard.
131
+
116
132
  Usage: click **Record**, move the pointer over the canvas to draw the UFO's path, click **Stop**, then **Play** to
117
133
  replay it. The nested `<rr0-ufo>`'s `enableClickToPlay` is set to `false` here — a completed recording drag also
118
134
  fires a native "click" on the canvas, which would otherwise spuriously toggle playback right after recording.
@@ -123,8 +139,9 @@ Duration) are translated (English/French) the same way `<rr0-ufo>`'s own labels
123
139
 
124
140
  | Member | Kind | Description |
125
141
  |---|---|---|
142
+ | `src` | attribute | URL of a `SightingRecordingJson` to open in the editor, fetched on connect and whenever the attribute changes |
126
143
  | `sightingData` | property (get/set) | Delegates to the nested `<rr0-ufo>`'s `sightingData` |
127
- | `appearance` | property (get/set, accepts a partial object on set) | `{ presetId: "oval" \| "saucer" \| "triangle", color: string, transparency: number, haloScale: number }` — the UFO's appearance used for the next recording |
144
+ | `appearance` | property (get/set, accepts a partial object on set) | `{ presetId: "oval" \| "polygon", color: string, transparency: number, haloScale: number }` — the UFO's appearance used for the next recording |
128
145
 
129
146
  ## `<rr0-scene>` — 3D decor
130
147
 
@@ -208,10 +225,10 @@ URL (typically relative to the case's own page, same as `<rr0-ufo>`'s own `src`)
208
225
 
209
226
  The two shapes are told apart automatically — a fetched JSON array is a manifest, a plain object is one witness's
210
227
  own recording. No labels or ids are duplicated in a manifest itself — each witness's display name and the shared
211
- case id grouping them together are read from that witness's *own* file (`witnessName`/`caseId`, see
228
+ case id grouping them together are read from that witness's *own* file (`witness`/`caseId`, see
212
229
  [Data format](#data-format)), so there's a single source of truth and nothing to drift out of sync. This means
213
230
  every listed witness's recording is fetched upfront (to read its name), not lazily on selection — fine at the
214
- scale a case's witness list actually has. If a witness has no `witnessName`, its `witnessId` is shown instead, or
231
+ scale a case's witness list actually has. If a witness has no `witness.title`, its `witness.id` is shown instead, or
215
232
  the URL itself as a last resort. A mismatched `caseId` across the listed witnesses logs a console warning (doesn't
216
233
  block) — likely means unrelated recordings got listed together by mistake.
217
234
 
@@ -229,13 +246,35 @@ selector loads that witness's already-fetched recording into the nested `<rr0-sc
229
246
  `witnessUrls` again (e.g. a manifest refresh) keeps the current selection if that witness is still present, instead
230
247
  of resetting back to the first.
231
248
 
232
- Clicking "?" opens a panel, anchored to the button as a floating overlay (it never shifts the canvas below it).
233
- Its main content is the currently-selected witness's observation metadata (date, location, case id whichever are
234
- actually present in that witness's own `sighting.json`; the witness's own name isn't repeated here, since it's
235
- already in the toolbar's testimony line). Below that, a smaller footer row holds the app's own name/version on the
236
- left, linking to [ufoathome.org](https://ufoathome.org), and a "Credits" link on the right that reveals third-party
237
- credits on click (the live terrain imagery attribution, once a real relief patch has resolved, plus the bundled
238
- thunder sound's own required attribution see [`CREDITS.md`](CREDITS.md)).
249
+ Clicking "?" opens a panel anchored under the button (it never shifts the canvas below it). Where the browser has
250
+ the popover API the panel is a top-layer `popover="auto"` the one placement a host page's own `overflow: hidden`
251
+ wrapper cannot clip, which is what rr0.org's layout was doing to it kept under the button by CSS anchor
252
+ positioning, flipping above it or centring in the viewport when that side is too short, and closing on Escape or a
253
+ click outside. Browsers without the API get the plain absolutely-positioned overlay instead.
254
+
255
+ Its main content is the currently-selected witness's observation metadata (date, location, case id, description,
256
+ tags — whichever are actually present in that witness's own `sighting.json`; the witness's own name isn't repeated
257
+ here, since it's already in the toolbar's testimony line). The date is shown on the WITNESS's own clock, never
258
+ converted into the reader's time zone (see `utcOffsetHours` in [Data format](#data-format)).
259
+
260
+ A footer row holds the app's own name/version on the left — linking to that very observation in the editor (see
261
+ [`<rr0-ufo-recorder>`](#rr0-ufo-recorder--full-editor)'s own `src`), not to the application's home page — and two
262
+ fold-outs on the right, both closed until asked for:
263
+
264
+ - **Embed** hands out the two self-contained lines it takes to put this observation on any other page, either as a
265
+ replay (`<rr0-eyewitness>`) or as the editor (`<rr0-ufo-recorder>`), with absolute URLs and a copy button:
266
+
267
+ ```html
268
+ <script type="module" src="https://rr0.org/science/crypto/ufo/rr0-eyewitness.mjs"></script>
269
+ <rr0-eyewitness src="https://rr0.org/science/crypto/ufo/enquete/dossier/Socorro/sighting.json"></rr0-eyewitness>
270
+ ```
271
+
272
+ The script URL is derived from where the running bundle was itself loaded from (`import.meta.url`), never
273
+ hardcoded, so a snippet generated from a local or staging copy points back at that copy. Pasting it into a site
274
+ of your own needs the bundle and the recording to be readable cross-origin (rr0.org serves
275
+ `/science/crypto/ufo/*` with `Access-Control-Allow-Origin: *` for exactly this).
276
+ - **Credits** reveals third-party credits (the live terrain imagery attribution, once a real relief patch has
277
+ resolved, plus the bundled thunder sound's own required attribution — see [`CREDITS.md`](CREDITS.md)).
239
278
 
240
279
  All of this component's own labels (Testimony by, About, Close, Observation/Date/Location/Case, Credits) are
241
280
  translated (English/French) the same way as `<rr0-ufo>`'s own labels.
@@ -250,10 +289,12 @@ interface SightingRecordingJson {
250
289
  time?: { year?: number, month?: number, day?: number, hour?: number, minute?: number, second?: number }
251
290
  endTime?: { year?: number, month?: number, day?: number, hour?: number, minute?: number, second?: number } // alternative to durationSeconds
252
291
  durationSeconds?: number // alternative to endTime; takes precedence if both are set
292
+ utcOffsetHours?: number // the LEGAL time zone the witness's clock was on (+1 for France in 1965, -7 for New Mexico in April 1964). Absent = approximated from the longitude, which cannot know legal time or a daylight-saving switch
253
293
  place?: { lat: number, lng: number }[]
254
- witnessId?: string // opaque internal referenceno PII beyond a display name (see witnessName)
255
- witnessName?: string // for cases where the witness is already publicly named in the published material (e.g. Chiles-Whitted) — omit for anonymous witnesses
294
+ witness?: { id?: string, dirName?: string, title?: string, lastName?: string, firstNames?: string[] } // every field optionalsupply whichever is known; omit entirely for an anonymous witness
256
295
  caseId?: string // shared by every witness's own sighting.json for the same case — see <rr0-eyewitness>
296
+ description?: string
297
+ tags?: string[]
257
298
  timeline: {
258
299
  keyframes: Array<{
259
300
  t: number // milliseconds since recording start
@@ -267,14 +308,35 @@ interface SightingRecordingJson {
267
308
  transparency: number // 0 = opaque, 1 = fully transparent
268
309
  haloScale: number // 0 = no glow
269
310
  selected: boolean
311
+ title?: string // shown as an on-canvas tooltip when hovered
312
+ physical?: { sizeM: number, distanceM: number } // what the witness reported — see Apparent size
270
313
  points?: { x: number, y: number }[] // "polygon" shapes only
271
314
  }
272
315
  }>
273
316
  }>
317
+ order?: string[] // back-to-front paint/hit-test order; absent = first-appearance order
318
+ groups?: string[][] // each inner array is one group's member sourceIds
274
319
  }
320
+ witnessTrack?: { keyframes: Array<{ t: number, pose: { lat?: number, lng?: number, elevationM: number, headingDeg?: number, pitchDeg: number, fovDeg: number } }> }
321
+ weatherTrack?: { keyframes: Array<{ t: number, weather: Weather }> }
322
+ weather?: Weather // legacy static fallback for recordings predating weatherTrack
323
+ decor?: DecorObject[] // buildings, trees, streetlights, vehicles, other witnesses — see src/engine/model/Decor.ts
275
324
  }
276
325
  ```
277
326
 
327
+ A shape left out of a later keyframe is **held** at its last recorded state, not hidden — and one whose first
328
+ keyframe is at `t=5000` is already painted, in that state, from `t=0` (hold-first/hold-last at both ends of a
329
+ source's own range). To make something stop being visible, keyframe it with `transparency: 1`.
330
+
331
+ ### Apparent size
332
+
333
+ `physical` is the witness's own reported size and distance, and the on-screen `bounds.width` is derived from it
334
+ rather than drawn by eye: on the 640x360 canvas at the default 60° vertical field of view, one degree is about
335
+ 5.4px and the full Moon about 2.8px, so an object of 3.5m at 90m is 12px wide — not the 90px an author reaches for
336
+ unaided. `ApparentSize` (`src/engine/shape/ApparentSize.ts`) does the conversion both ways, and the editor's own
337
+ **Real size** / **Distance** fields apply it and read back what the current drawing actually spans, in degrees and
338
+ in full Moons.
339
+
278
340
  This format is deliberately independent of [`@rr0/data`](https://github.com/RR0/data)'s `RR0Event`/`@rr0/time`'s
279
341
  `Level2Date`/`@rr0/place`'s `Place` classes, even though its `time`/`place` fields are structurally aligned with
280
342
  them — importing those classes into browser-bundled code pulls in Node-only file-scanning dependencies that break a
@@ -0,0 +1 @@
1
+ const e={oval:"Ovale",polygon:"Polygone",addVertex:"Ajouter un sommet",deleteVertex:"Supprimer le sommet",notAPolygon:"Sélectionnez une seule forme polygonale",tooFewVertices:"Une forme a besoin d'au moins 3 points",color:"Couleur",transparency:"Transparence",halo:"Halo",shape:"Forme",shapeTitle:"Nom",objectSize:"Taille réelle",objectDistance:"Distance",apparentSize:"soit {deg}° — {moons}× la Lune",utcOffset:"Fuseau horaire",utcOffsetPlaceholder:"d'après la longitude",objectSizePlaceholder:"largeur rapportée",objectDistancePlaceholder:"distance rapportée",addShape:"Ajouter une forme",deleteShape:"Supprimer la forme",play:"Lecture",pause:"Pause",noDuration:"Aucune durée d'observation",autoReplay:"Lecture automatique",group:"Grouper",ungroup:"Dégrouper",bringToFront:"Placer devant",sendToBack:"Placer derrière",contextMenuDelete:"Supprimer",confirmDeleteShape:"Supprimer {name} ? Cette action est irréversible.",confirmDeleteShapes:"Supprimer {count} formes ? Cette action est irréversible.",onlyOneShape:"Il n'y a qu'une seule forme",alreadyAtFront:"Cette forme est déjà la plus en avant",alreadyAtBack:"Cette forme est déjà la plus en arrière",needTwoShapesToGroup:"Sélectionnez au moins deux formes pour les grouper",notGrouped:"Cette forme ne fait pas partie d'un groupe",multipleShapesSelected:"Plusieurs formes sélectionnées — sélectionnez-en une seule pour modifier ceci",samplingRate:"Échantillonnage",duration:"Durée",durationPlaceholder:"durée de l'observation",durationImprecise:"Ces dates ne permettent pas de déterminer précisément une durée — saisissez-la manuellement.",export:"Exporter le JSON",importFile:"Charger un fichier JSON",importUrl:"Ou charger depuis une URL",importUrlPlaceholder:"https://…/sighting.json",importButton:"Charger",importError:"Impossible de charger cet enregistrement — vérifiez le fichier ou l'URL et réessayez.",record:"Enregistrer",stop:"Arrêter",latitude:"Latitude",longitude:"Longitude",heading:"Orientation",headingPlaceholder:"inconnu",pitch:"Inclinaison",observationTime:"Début de l'observation",observationEndTime:"Fin de l'observation",edtfInvalid:"Date/heure EDTF invalide (ex. 1965-07-01T05:00, 2025-06?, 2025~).",edtfPlaceholder:"AAAA-MM-JJThh:mm[?~%] ou hh:mm",observationTimeHint:"EDTF — ex. 1965-07-01T05:00, 2025-06? (incertain), 2025~ (approximatif), ou juste 05:00 si la date est inconnue",observationEndTimeHint:"EDTF — ex. 1965-07-01T05:10, 2025-06? (incertain), 2025~ (approximatif), ou juste 05:10 si la date est inconnue",presetsGroupLabel:"Forme de l'ovni",witnessId:"ID témoin",witnessDirName:"Répertoire du témoin",witnessTitle:"Titre du témoin",witnessLastName:"Nom de famille du témoin",witnessFirstNames:"Prénoms du témoin",caseId:"ID de l'affaire",description:"Description",tags:"Mots-clés",tagsPlaceholder:"séparés par des virgules",weather:"Météo",shapeGroup:"Forme",temporalGroup:"Temporel",locationGroup:"Lieu",observationGroup:"Observation",witnessGroup:"Témoin",circumstancesGroup:"Circonstances",cloudCover:"Couverture nuageuse",cloudDarkness:"Obscurité des nuages",precipitationType:"Précipitations",precipitationNone:"Aucune",precipitationRain:"Pluie",precipitationSnow:"Neige",precipitationHail:"Grêle",precipitationIntensity:"Intensité",windDirection:"Direction du vent",windSpeed:"Force du vent",storm:"Orage",lensFlareBrightness:"Intensité lumineuse",cameraDevice:"Appareil photo/vidéo",decor:"Décor",decorBuilding:"Bâtiment",decorTree:"Arbre",decorStreetlight:"Lampadaire",decorVehicle:"Véhicule",decorWitness:"Autre témoin",addDecor:"Ajouter",deleteDecor:"Supprimer l'élément de décor",decorEast:"Distance à l'est",decorNorth:"Distance au nord",decorHeading:"Orientation",decorLit:"Allumé",decorTitle:"Nom",decorSightingUrl:"URL de l'enregistrement du témoin",viewTestimony:"Voir le témoignage",noWitnessRecording:"Aucune URL d'enregistrement définie pour ce témoin",masks:"Masque",addWitness:"Ajouter un témoin",decorFloors:"Étages",decorOccupiedFloor:"Étage occupé",decorWitnessSide:"Emplacement du témoin",decorWitnessSideNone:"Non présent",decorWindows:"Fenêtres",decorSideFront:"Avant",decorSideBehind:"Arrière",decorSideLeft:"Gauche",decorSideRight:"Droite",decorSideFrontLeft:"Avant-gauche",decorSideFrontRight:"Avant-droit",decorSideBehindLeft:"Arrière-gauche",decorSideBehindRight:"Arrière-droit"};export{e as ufoRecorderMessages_fr};
@@ -261,11 +261,15 @@ input.invalid {
261
261
  }
262
262
  /* Record and the sampling rate it records at are one control, not two: grouping them makes them a
263
263
  single flex item of the wrapping toolbar, so they stay on the same line together wherever that
264
- line happens to break, instead of the button drifting up next to whatever field precedes it. */
264
+ line happens to break, instead of the button drifting up next to whatever field precedes it.
265
+ flex-basis: 100% then gives them that line outright, always — the pair is wider than whatever
266
+ the appearance fields leave at the end of theirs, so it was going to wrap anyway; taking the
267
+ whole line makes that a deliberate row rather than a break that moves with the window width. */
265
268
  .record-row {
266
269
  display: flex;
267
270
  align-items: center;
268
271
  gap: 0.5em;
272
+ flex-basis: 100%;
269
273
  }
270
274
  details {
271
275
  border: 1px solid #444;
@@ -691,7 +695,7 @@ input[type=range] {
691
695
  pointer-events: none;
692
696
  white-space: nowrap;
693
697
  }
694
- `;class fs{static MOON_ANGULAR_WIDTH_DEG=.5237;static angularWidthDeg(e){return 2*Math.atan(e.sizeM/(2*e.distanceM))*180/Math.PI}static widthPx(e,t,n){return t*e.sizeM/(2*e.distanceM*Math.tan(n*Math.PI/360))}static pxToDeg(e,t,n){return 2*Math.atan(e*Math.tan(n*Math.PI/360)/t)*180/Math.PI}static inMoons(e){return e/fs.MOON_ANGULAR_WIDTH_DEG}static lerp(e,t,n){if(!(!e||!t))return{sizeM:e.sizeM+(t.sizeM-e.sizeM)*n,distanceM:e.distanceM+(t.distanceM-e.distanceM)*n}}}function I0(i,e="#39ff14"){return{kind:"oval",bounds:i,color:e,angle:0,transparency:0,haloScale:0,selected:!1}}function R0(i,e,t="#39ff14"){return{kind:"polygon",bounds:i,points:e,color:t,angle:0,transparency:0,haloScale:0,selected:!1}}function C0(i,e="#39ff14"){const{width:t,height:n}=i;return R0(i,[{x:0,y:0},{x:t,y:0},{x:t,y:n},{x:0,y:n}],e)}const D0={oval:I0,polygon:C0};function Gc(i,e){return{...D0[e.presetId](i,e.color),transparency:e.transparency,haloScale:e.haloScale}}function P0(i,e,t){const{bounds:n}=i;return e>=n.x&&e<=n.x+n.width&&t>=n.y&&t<=n.y+n.height}function Yd(i,e,t){const n=i.bounds.width,s=i.bounds.height;return{...i,bounds:{x:e-n/2,y:t-s/2,width:n,height:s}}}function Qt(i,e,t){return i+(e-i)*t}function Hc(i){const e=/^#([0-9a-f]{6})$/i.exec(i);if(!e)return;const t=parseInt(e[1],16);return[t>>16&255,t>>8&255,t&255]}function L0(i){return`#${i.map(e=>Math.round(e).toString(16).padStart(2,"0")).join("")}`}function N0(i,e,t){const n=Hc(i),s=Hc(e);return!n||!s?t<1?i:e:L0([Qt(n[0],s[0],t),Qt(n[1],s[1],t),Qt(n[2],s[2],t)])}function U0(i,e,t){const n={x:Qt(i.bounds.x,e.bounds.x,t),y:Qt(i.bounds.y,e.bounds.y,t),width:Qt(i.bounds.width,e.bounds.width,t),height:Qt(i.bounds.height,e.bounds.height,t)},s=Qt(i.angle,e.angle,t),r=Qt(i.transparency,e.transparency,t),a=Qt(i.haloScale,e.haloScale,t),o=N0(i.color,e.color,t),l=fs.lerp(i.physical,e.physical,t);return i.kind==="polygon"&&e.kind==="polygon"&&i.points.length===e.points.length?{...i,bounds:n,angle:s,transparency:r,haloScale:a,color:o,physical:l,points:i.points.map((c,h)=>({x:Qt(c.x,e.points[h].x,t),y:Qt(c.y,e.points[h].y,t)}))}:{...t<1?i:e,bounds:n,angle:s,transparency:r,haloScale:a,color:o,physical:l}}class wa{keyframes=[];order=[];groups=[];addKeyframe(e,t){const n=this.findInsertIndex(e);if(this.keyframes[n]?.t===e){const s=new Set(t.map(r=>r.sourceId));this.keyframes[n]={t:e,shapes:[...this.keyframes[n].shapes.filter(r=>!s.has(r.sourceId)),...t]}}else this.keyframes.splice(n,0,{t:e,shapes:[...t]});for(const s of t)this.order.includes(s.sourceId)||this.order.push(s.sourceId)}findInsertIndex(e){let t=0,n=this.keyframes.length;for(;t<n;){const s=t+n>>>1;this.keyframes[s].t<e?t=s+1:n=s}return t}getKeyframeAt(e){const t=this.findInsertIndex(e),n=this.keyframes[t];return n?.t===e?n:void 0}getShapeAt(e,t){return this.getKeyframeAt(e)?.shapes.find(n=>n.sourceId===t)?.shape}getLatestShapeAt(e,t){let n=this.findInsertIndex(e);for(this.keyframes[n]?.t!==e&&(n-=1);n>=0;n--){const s=this.keyframes[n].shapes.find(r=>r.sourceId===t);if(s)return s.shape}}getInterpolatedShapeAt(e,t){const n=this.findShapeAtOrBefore(e,t);if(n?.t===e)return n.shape;const s=this.findShapeAtOrAfter(e,t);return n?s?U0(n.shape,s.shape,(e-n.t)/(s.t-n.t)):n.shape:s?.shape}findShapeAtOrBefore(e,t){let n=this.findInsertIndex(e);for(this.keyframes[n]?.t!==e&&(n-=1);n>=0;n--){const s=this.keyframes[n].shapes.find(r=>r.sourceId===t);if(s)return{t:this.keyframes[n].t,shape:s.shape}}}findShapeAtOrAfter(e,t){for(let n=this.findInsertIndex(e);n<this.keyframes.length;n++){const s=this.keyframes[n].shapes.find(r=>r.sourceId===t);if(s)return{t:this.keyframes[n].t,shape:s.shape}}}hitTest(e,t,n,s){const r=this.sourceIds;for(let a=r.length-1;a>=0;a--){if(s?.has(r[a]))continue;const o=this.getInterpolatedShapeAt(e,r[a]);if(o&&P0(o,t,n))return{sourceId:r[a],shape:o}}}removeSource(e){for(let t=this.keyframes.length-1;t>=0;t--){const n=this.keyframes[t].shapes.filter(s=>s.sourceId!==e);n.length===0?this.keyframes.splice(t,1):n.length!==this.keyframes[t].shapes.length&&(this.keyframes[t]={t:this.keyframes[t].t,shapes:n})}this.order=this.order.filter(t=>t!==e),this.removeFromGroup(e)}group(e){if(!(e.length<2)){for(const t of e)this.removeFromGroup(t);this.groups.push([...e])}}ungroup(e){const t=this.groups.findIndex(n=>n.includes(e));t!==-1&&this.groups.splice(t,1)}groupMembers(e){return this.groups.find(t=>t.includes(e))}removeFromGroup(e){const t=this.groups.findIndex(s=>s.includes(e));if(t===-1)return;const n=this.groups[t].filter(s=>s!==e);n.length<2?this.groups.splice(t,1):this.groups[t]=n}bringToFront(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.push(e))}sendToBack(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.unshift(e))}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get sourceIds(){return[...this.order]}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes,order:this.order,groups:this.groups}}static fromJSON(e){const t=new wa;for(const n of e.keyframes)t.addKeyframe(n.t,n.shapes);if(e.order){const n=t.order.filter(s=>!e.order.includes(s));t.order=[...e.order,...n]}return e.groups&&(t.groups=e.groups.map(n=>n.filter(s=>t.order.includes(s))).filter(n=>n.length>=2)),t}}function F0(i,e,t){return Math.max(e,Math.min(t,i))}function O0(i,e,t){const n=((e-i)%360+540)%360-180;return((i+n*t)%360+360)%360}function Ts(i,e,t){return i+(e-i)*t}function B0(i,e,t){return{lat:i.lat===void 0||e.lat===void 0?void 0:Ts(i.lat,e.lat,t),lng:i.lng===void 0||e.lng===void 0?void 0:Ts(i.lng,e.lng,t),elevationM:Ts(i.elevationM,e.elevationM,t),headingDeg:i.headingDeg===void 0||e.headingDeg===void 0?void 0:O0(i.headingDeg,e.headingDeg,t),pitchDeg:Ts(i.pitchDeg,e.pitchDeg,t),fovDeg:Ts(i.fovDeg,e.fovDeg,t)}}class Js{keyframes=[];addKeyframe(e,t){const n=this.findInsertIndex(e);this.keyframes[n]?.t===e?this.keyframes[n]={t:e,pose:t}:this.keyframes.splice(n,0,{t:e,pose:t})}clear(){this.keyframes.length=0}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}findInsertIndex(e){let t=0,n=this.keyframes.length;for(;t<n;){const s=t+n>>>1;this.keyframes[s].t<e?t=s+1:n=s}return t}getLatestPoseAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].pose:void 0}getInterpolatedPoseAt(e){const t=this.findInsertIndex(e),n=this.keyframes[t]?.t===e?this.keyframes[t]:this.keyframes[t-1];if(n?.t===e)return n.pose;const s=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return n?s?B0(n.pose,s.pose,F0((e-n.t)/(s.t-n.t),0,1)):n.pose:s?.pose}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes}}static fromJSON(e){const t=new Js;for(const n of e.keyframes)t.addKeyframe(n.t,n.pose);return t}}function cr(i,e,t){return i+(e-i)*t}function z0(i,e,t){const n=((e-i)%360+540)%360-180;return((i+n*t)%360+360)%360}function k0(i,e,t){return Math.max(e,Math.min(t,i))}function G0(i,e,t){return{cloudCover:cr(i.cloudCover,e.cloudCover,t),cloudDarkness:cr(i.cloudDarkness,e.cloudDarkness,t),precipitationType:t<1?i.precipitationType:e.precipitationType,precipitationIntensity:cr(i.precipitationIntensity,e.precipitationIntensity,t),windDirectionDeg:z0(i.windDirectionDeg,e.windDirectionDeg,t),windSpeed:cr(i.windSpeed,e.windSpeed,t),storm:t<1?i.storm:e.storm}}class Qs{keyframes=[];addKeyframe(e,t){const n=this.findInsertIndex(e);this.keyframes[n]?.t===e?this.keyframes[n]={t:e,weather:t}:this.keyframes.splice(n,0,{t:e,weather:t})}clear(){this.keyframes.length=0}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}findInsertIndex(e){let t=0,n=this.keyframes.length;for(;t<n;){const s=t+n>>>1;this.keyframes[s].t<e?t=s+1:n=s}return t}getLatestWeatherAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].weather:void 0}getInterpolatedWeatherAt(e){const t=this.findInsertIndex(e),n=this.keyframes[t]?.t===e?this.keyframes[t]:this.keyframes[t-1];if(n?.t===e)return n.weather;const s=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return n?s?G0(n.weather,s.weather,k0((e-n.t)/(s.t-n.t),0,1)):n.weather:s?.weather}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes}}static fromJSON(e){const t=new Qs;for(const n of e.keyframes)t.addKeyframe(n.t,n.weather);return t}}const qd={cloudCover:0,cloudDarkness:0,precipitationType:"none",precipitationIntensity:0,windDirectionDeg:0,windSpeed:0,storm:!1},H0=/^(?:(?<year>\d{4}|\d{3}X|\d{2}XX)(-(?<month>0[1-9]|1[0-2]))?(-(?<day>0[1-9]|[12]\d|3[01]))?(?:[T ](?<hour>[01]?\d|2[0-3]):(?<minute>[0-5]\d)(?::(?<second>[0-5]\d))?)?|(?<timeOnlyHour>[01]?\d|2[0-3]):(?<timeOnlyMinute>[0-5]\d)(?::(?<timeOnlySecond>[0-5]\d))?)[?~%]?$/;function Vc(i){const e=H0.exec(i.trim());if(!e?.groups)return;const{year:t,month:n,day:s,hour:r,minute:a,second:o,timeOnlyHour:l,timeOnlyMinute:c,timeOnlySecond:h}=e.groups,u=r??l,d=a??c,p=o??h;return{year:t&&!t.includes("X")?Number(t):void 0,month:n?Number(n):void 0,day:s?Number(s):void 0,hour:u?Number(u):void 0,minute:d?Number(d):void 0,second:p?Number(p):void 0,raw:i.trim()}}function Wc(i){if(i.raw!==void 0)return i.raw;if(i.year===void 0){if(i.hour===void 0)return"";let t=`${String(i.hour).padStart(2,"0")}:${String(i.minute??0).padStart(2,"0")}`;return i.second!==void 0&&(t+=`:${String(i.second).padStart(2,"0")}`),t}let e=String(i.year).padStart(4,"0");return i.month===void 0||(e+=`-${String(i.month).padStart(2,"0")}`,i.day===void 0)||(e+=`-${String(i.day).padStart(2,"0")}`,i.hour===void 0)||(e+=`T${String(i.hour).padStart(2,"0")}:${String(i.minute??0).padStart(2,"0")}`,i.second!==void 0&&(e+=`:${String(i.second).padStart(2,"0")}`)),e}function Oo(i){if(i.year!==void 0)return Date.UTC(i.year,(i.month??1)-1,i.day??1,i.hour??0,i.minute??0,i.second??0)}function Xc(i){return(((i.day??0)*24+(i.hour??0))*60+(i.minute??0))*6e4+(i.second??0)*1e3}const V0=["year","month","day","hour","minute"],W0=["day","hour","minute"];function Yc(i,e){return e.filter(t=>i[t]!==void 0).join(",")}function lc(i){if(i.durationSeconds!==void 0)return i.durationSeconds*1e3;if(!i.time||!i.endTime)return;const e=i.time.year!==void 0&&i.endTime.year!==void 0,t=e?V0:W0,n=Yc(i.time,t);if(!(n===""||n!==Yc(i.endTime,t)))return e?Oo(i.endTime)-Oo(i.time):Xc(i.endTime)-Xc(i.time)}function X0(i){if(i.durationSeconds===void 0&&!(!i.time||!i.endTime))return lc(i)===void 0?"imprecise":void 0}class Ta{constructor(e,t,n,s,r,a,o,l=[]){this.event=e,this.timeline=t,this.witnessTrack=n,this.weatherTrack=s,this.witness=r,this.caseId=a,this.weather=o,this.decor=l}static create(e,t,n){return new Ta({eventType:"sighting",time:e,place:t},new wa,new Js,new Qs,n)}}const Y0=0,q0=0,$0=60;function Bo(i,e){const t=i.witnessTrack.getInterpolatedPoseAt(e);if(t)return t;const n=i.event.place?.[0];if(n)return{lat:n.lat,lng:n.lng,elevationM:Y0,headingDeg:void 0,pitchDeg:q0,fovDeg:$0}}function zo(i,e){return i.weatherTrack.getInterpolatedWeatherAt(e)??i.weather??qd}class K0{constructor(e,t){this.timeline=e,this.onFrame=t}rafId=null;state="stopped";currentT=0;lastWallTime=0;playbackRate=1;loop=!1;durationOverrideMs=0;get seekableDuration(){return Math.max(this.timeline.duration,this.durationOverrideMs)}play(){if(this.state==="playing")return;this.currentT>=this.seekableDuration&&(this.currentT=0,this.resolveFrame(0)),this.state="playing",this.lastWallTime=performance.now();const e=()=>{if(this.state!=="playing")return;const t=performance.now();if(this.currentT+=(t-this.lastWallTime)*this.playbackRate,this.lastWallTime=t,this.currentT>=this.seekableDuration){if(this.loop&&this.seekableDuration>0){this.currentT%=this.seekableDuration,this.resolveFrame(this.currentT),this.rafId=requestAnimationFrame(e);return}this.currentT=this.seekableDuration,this.stop(),this.resolveFrame(this.currentT);return}this.resolveFrame(this.currentT),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}pause(){this.state==="playing"&&(this.state="paused",this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null))}stop(){this.state="stopped",this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}seek(e){this.currentT=Math.max(0,Math.min(e,this.seekableDuration)),this.resolveFrame(this.currentT)}get playbackState(){return this.state}get time(){return this.currentT}resolveFrame(e){const t=new Map;for(const n of this.timeline.sourceIds){const s=this.timeline.getInterpolatedShapeAt(e,n);s&&t.set(n,s)}this.onFrame(e,t)}}const $d=["nw","n","ne","e","se","s","sw","w"],Z0=24,ri=8,Kd=3,J0=8;function Hs(i,e,t){const n=i.x-e.x,s=i.y-e.y,r=Math.cos(t),a=Math.sin(t);return{x:e.x+n*r-s*a,y:e.y+n*a+s*r}}function Si(i){return{x:i.x+i.width/2,y:i.y+i.height/2}}const Q0={nw:{left:!0,top:!0},n:{top:!0},ne:{right:!0,top:!0},e:{right:!0},se:{right:!0,bottom:!0},s:{bottom:!0},sw:{left:!0,bottom:!0},w:{left:!0}};class Ke{static handlePointsFor(e){const{x:t,y:n,width:s,height:r}=e.bounds,a={nw:{x:t,y:n},n:{x:t+s/2,y:n},ne:{x:t+s,y:n},e:{x:t+s,y:n+r/2},se:{x:t+s,y:n+r},s:{x:t+s/2,y:n+r},sw:{x:t,y:n+r},w:{x:t,y:n+r/2},rotate:{x:t+s/2,y:n-Z0}},o=Si(e.bounds),l={};for(const c of Object.keys(a))l[c]=Hs(a[c],o,e.angle);return l}static hitTestHandle(e,t,n=8,s=[...$d,"rotate"]){const r=Ke.handlePointsFor(e);for(const a of s)if(Math.hypot(t.x-r[a].x,t.y-r[a].y)<=n)return a}static HANDLE_DIRECTION_DEG={e:0,se:45,s:90,sw:135,w:180,nw:225,n:270,ne:315};static resizeAxisFor(e,t){const s=((Ke.HANDLE_DIRECTION_DEG[e]+t*180/Math.PI)%180+180)%180/45,r=["ew","nwse","ns","nesw"];return r[Math.round(s)%r.length]}static resizeBounds(e,t,n,s){const r=Si(e),a=Hs(s,r,-t),{x:o,y:l,width:c,height:h}=e,u=Q0[n];let d=o,p=l,v=o+c,y=l+h;return u.left&&(d=Math.min(a.x,v-ri)),u.right&&(v=Math.max(a.x,d+ri)),u.top&&(p=Math.min(a.y,y-ri)),u.bottom&&(y=Math.max(a.y,p+ri)),{x:d,y:p,width:v-d,height:y-p}}static resizeShape(e,t,n){if(t==="rotate")throw new Error("resizeShape does not accept the rotate handle");const{width:s,height:r}=e.bounds,a=Ke.resizeBounds(e.bounds,e.angle,t,n);if(e.kind==="oval")return{...e,bounds:a};const o=s===0?1:a.width/s,l=r===0?1:a.height/r;return{...e,bounds:a,points:e.points.map(c=>({x:c.x*o,y:c.y*l}))}}static rotateShape(e,t){const n=Si(e.bounds),s=Math.atan2(t.y-n.y,t.x-n.x)+Math.PI/2;return{...e,angle:s}}static groupBoundsFor(e){const t=Math.min(...e.map(a=>a.x)),n=Math.min(...e.map(a=>a.y)),s=Math.max(...e.map(a=>a.x+a.width)),r=Math.max(...e.map(a=>a.y+a.height));return{x:t,y:n,width:s-t,height:r-n}}static vertexPointsFor(e){const t=Si(e.bounds);return e.points.map(n=>Hs({x:e.bounds.x+n.x,y:e.bounds.y+n.y},t,e.angle))}static hitTestVertex(e,t,n=J0){const s=Ke.vertexPointsFor(e);let r,a=n;return s.forEach((o,l)=>{const c=Math.hypot(t.x-o.x,t.y-o.y);c<=a&&(a=c,r=l)}),r}static toLocalPoint(e,t){const n=Si(e.bounds),s=Hs(t,n,-e.angle);return{x:s.x-e.bounds.x,y:s.y-e.bounds.y}}static moveVertex(e,t,n){const s=e.points.map((u,d)=>d===t?Ke.toLocalPoint(e,n):u),r=Math.min(...s.map(u=>u.x)),a=Math.min(...s.map(u=>u.y)),o=Math.max(...s.map(u=>u.x)),l=Math.max(...s.map(u=>u.y)),c={x:e.bounds.x+r,y:e.bounds.y+a,width:o-r,height:l-a},h=s.map(u=>({x:u.x-r,y:u.y-a}));return{...e,bounds:c,points:h}}static insertVertexNear(e,t){const n=Ke.toLocalPoint(e,t),{points:s}=e;let r=s.length-1,a=1/0;for(let l=0;l<s.length;l++){const c=s[l],h=s[(l+1)%s.length],u=Ke.distanceToSegment(n,c,h);u<a&&(a=u,r=l)}const o=[...s.slice(0,r+1),n,...s.slice(r+1)];return{...e,points:o}}static distanceToSegment(e,t,n){const s=n.x-t.x,r=n.y-t.y,a=s*s+r*r,o=a===0?0:Math.max(0,Math.min(1,((e.x-t.x)*s+(e.y-t.y)*r)/a)),l=t.x+o*s,c=t.y+o*r;return Math.hypot(e.x-l,e.y-c)}static deleteVertex(e,t){return e.points.length<=Kd?e:{...e,points:e.points.filter((n,s)=>s!==t)}}}class Ba{constructor(e){this.members=e}bounds(){return Ke.groupBoundsFor(this.members.map(e=>e.shape.bounds))}scaleTo(e){const t=this.bounds(),n=t.width===0?1:e.width/t.width,s=t.height===0?1:e.height/t.height;return this.members.map(({sourceId:r,shape:a})=>{const o=Math.max(a.bounds.width*n,ri),l=Math.max(a.bounds.height*s,ri),c=e.x+(a.bounds.x-t.x)*n,h=e.y+(a.bounds.y-t.y)*s,u={x:c,y:h,width:o,height:l};return a.kind==="oval"?{sourceId:r,shape:{...a,bounds:u}}:{sourceId:r,shape:{...a,bounds:u,points:a.points.map(d=>({x:d.x*n,y:d.y*s}))}}})}resize(e,t){return this.scaleTo(Ke.resizeBounds(this.bounds(),0,e,t))}rotate(e,t){const n=Si(this.bounds()),s=a=>Math.atan2(a.y-n.y,a.x-n.x),r=s(e)-s(t);return this.members.map(({sourceId:a,shape:o})=>{const l=Si(o.bounds),c=Hs(l,n,r),h={x:c.x-o.bounds.width/2,y:c.y-o.bounds.height/2,width:o.bounds.width,height:o.bounds.height};return{sourceId:a,shape:{...o,bounds:h,angle:o.angle+r}}})}}const ko=6,hr=ko/2,j0=20,qc=4;class ef{constructor(e){this.ctx=e}clear(e,t){this.ctx.clearRect(0,0,e,t)}paintShape(e){this.ctx.save(),this.ctx.globalAlpha=1-e.transparency,e.haloScale>0&&this.paintHalo(e),this.paintBase(e),this.ctx.restore(),e.selected&&this.paintSelectionHandles(e)}paintBase(e){if(this.ctx.fillStyle=e.color,this.ctx.beginPath(),e.kind==="oval"){const{x:t,y:n,width:s,height:r}=e.bounds,a=s/2,o=r/2;this.ctx.ellipse(t+a,n+o,a,o,e.angle,0,2*Math.PI)}else{const{x:t,y:n,width:s,height:r}=e.bounds;this.ctx.save(),this.ctx.translate(t+s/2,n+r/2),this.ctx.rotate(e.angle),this.ctx.translate(-s/2,-r/2),e.points.forEach((a,o)=>{o===0?this.ctx.moveTo(a.x,a.y):this.ctx.lineTo(a.x,a.y)}),this.ctx.closePath(),this.ctx.restore()}this.ctx.fill()}paintHalo(e){this.ctx.save(),this.ctx.shadowColor=e.color,this.ctx.shadowBlur=j0*e.haloScale,this.paintBase(e),this.ctx.restore()}paintSelectionHandles(e){this.paintHandleFrame(Ke.handlePointsFor(e),{includeRotate:!0}),e.kind==="polygon"&&this.paintVertexHandles(e)}paintVertexHandles(e){this.ctx.fillStyle="#39f";for(const t of Ke.vertexPointsFor(e))this.ctx.beginPath(),this.ctx.ellipse(t.x,t.y,qc,qc,0,0,2*Math.PI),this.ctx.fill()}paintMemberOutline(e){this.paintOutline(Ke.handlePointsFor(e))}paintGroupHandles(e){this.paintHandleFrame(Ke.handlePointsFor({bounds:e,angle:0}),{includeRotate:!0})}paintOutline(e){this.ctx.strokeStyle="lightgray",this.ctx.beginPath(),this.ctx.moveTo(e.nw.x,e.nw.y),this.ctx.lineTo(e.ne.x,e.ne.y),this.ctx.lineTo(e.se.x,e.se.y),this.ctx.lineTo(e.sw.x,e.sw.y),this.ctx.closePath(),this.ctx.stroke()}paintHandleFrame(e,{includeRotate:t}){this.paintOutline(e),this.ctx.fillStyle="lightgray";for(const n of $d){const s=e[n];this.ctx.fillRect(s.x-hr,s.y-hr,ko,ko)}t&&(this.ctx.beginPath(),this.ctx.moveTo(e.n.x,e.n.y),this.ctx.lineTo(e.rotate.x,e.rotate.y),this.ctx.stroke(),this.ctx.beginPath(),this.ctx.ellipse(e.rotate.x,e.rotate.y,hr+1,hr+1,0,0,2*Math.PI),this.ctx.fill())}}function tf(i){return{version:1,time:i.event.time,endTime:i.event.endTime,durationSeconds:i.event.durationSeconds,utcOffsetHours:i.event.utcOffsetHours,place:i.event.place,witness:i.witness,caseId:i.caseId,description:i.event.description,tags:i.event.tags,timeline:i.timeline.toJSON(),witnessTrack:i.witnessTrack.toJSON(),weatherTrack:i.weatherTrack.toJSON(),weather:i.weather,decor:i.decor}}function nf(i){return new Ta({eventType:"sighting",time:i.time,endTime:i.endTime,durationSeconds:i.durationSeconds,utcOffsetHours:i.utcOffsetHours,place:i.place,description:i.description,tags:i.tags},wa.fromJSON(i.timeline),i.witnessTrack?Js.fromJSON(i.witnessTrack):new Js,i.weatherTrack?Qs.fromJSON(i.weatherTrack):new Qs,i.witness,i.caseId,i.weather,i.decor??[])}function Aa(i,e){for(const t of i){const n=t.toLowerCase().split("-")[0];if(e.includes(n))return n}return"en"}const sf="modulepreload",rf=function(i,e){return new URL(i,e).href},$c={},ha=function(e,t,n){let s=Promise.resolve();if(t&&t.length>0){let a=function(h){return Promise.all(h.map(u=>Promise.resolve(u).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const o=document.getElementsByTagName("link"),l=document.querySelector("meta[property=csp-nonce]"),c=l?.nonce||l?.getAttribute("nonce");s=a(t.map(h=>{if(h=rf(h,n),h in $c)return;$c[h]=!0;const u=h.endsWith(".css"),d=u?'[rel="stylesheet"]':"";if(!!n)for(let y=o.length-1;y>=0;y--){const m=o[y];if(m.href===h&&(!u||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${h}"]${d}`))return;const v=document.createElement("link");if(v.rel=u?"stylesheet":sf,u||(v.as="script"),v.crossOrigin="",v.href=h,c&&v.setAttribute("nonce",c),document.head.appendChild(v),u)return new Promise((y,m)=>{v.addEventListener("load",y),v.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${h}`)))})}))}function r(a){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=a,window.dispatchEvent(o),!o.defaultPrevented)throw a}return s.then(a=>{for(const o of a||[])o.status==="rejected"&&r(o.reason);return e().catch(r)})},Zd=["en","fr"],af={en:()=>ha(()=>Promise.resolve().then(()=>hf),void 0,import.meta.url).then(i=>i.ufoMessages_en),fr:()=>ha(()=>import("./UfoMessages_fr-NZrwNFCQ.js"),[],import.meta.url).then(i=>i.ufoMessages_fr)};function of(i){return af[i]()}const lf={en:()=>ha(()=>Promise.resolve().then(()=>oy),void 0,import.meta.url).then(i=>i.ufoRecorderMessages_en),fr:()=>ha(()=>import("./UfoRecorderMessages_fr-CUB6w4ij.js"),[],import.meta.url).then(i=>i.ufoRecorderMessages_fr)};function cf(i){return lf[i]()}const Jd={play:"Play",pause:"Pause",noDuration:"No observation duration",autoReplay:"Auto-replay",currentPosition:"Current position",duration:"Duration",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen"},hf=Object.freeze(Object.defineProperty({__proto__:null,ufoMessages_en:Jd},Symbol.toStringTag,{value:"Module"})),Kc=new Set;class df extends HTMLElement{static get observedAttributes(){return["src"]}shadow;stageElement;canvas;canvasRenderer;tooltip;toolbar;playPauseButton;loopButton;fullscreenButton;seekInput;timeStartLabel;timeEndLabel;currentSighting=Ta.create();player;loopEnabled=!0;highlightedSourceIds=new Set;occludedSourceIds=Kc;enableClickToPlay=!0;fullscreenTarget;messages=Jd;realDurationMs;realStartMs;handleFullscreenChange=()=>this.updateFullscreenButton();handlePointerMove=e=>{const t=this.canvasPointFromEvent(e),n=t&&this.currentSighting.timeline.hitTest(this.currentTime,t.x,t.y,this.occludedSourceIds);if(!n?.shape.title){this.tooltip.hidden=!0;return}this.tooltip.textContent=n.shape.title,this.tooltip.hidden=!1;const s=this.stageElement.getBoundingClientRect();this.tooltip.style.left=`${e.clientX-s.left+12}px`,this.tooltip.style.top=`${e.clientY-s.top+12}px`};handlePointerLeave=()=>{this.tooltip.hidden=!0};constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${A0}</style>${T0}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.stageElement=this.shadow.getElementById("stage"),this.canvas=this.shadow.getElementById("canvas"),this.canvasRenderer=new ef(this.canvas.getContext("2d")),this.tooltip=this.shadow.getElementById("tooltip"),this.toolbar=this.shadow.getElementById("toolbar"),this.playPauseButton=this.shadow.getElementById("play-pause"),this.loopButton=this.shadow.getElementById("loop"),this.fullscreenButton=this.shadow.getElementById("fullscreen"),this.seekInput=this.shadow.getElementById("seek"),this.timeStartLabel=this.shadow.getElementById("time-start"),this.timeEndLabel=this.shadow.getElementById("time-end"),this.fullscreenTarget=this.stageElement,this.playPauseButton.addEventListener("click",()=>this.togglePlayPause()),this.loopButton.addEventListener("click",()=>this.toggleLoop()),this.fullscreenButton.addEventListener("click",()=>this.toggleFullscreen()),this.seekInput.addEventListener("input",()=>this.player.seek(Number(this.seekInput.value))),this.canvas.addEventListener("click",()=>{this.enableClickToPlay&&this.togglePlayPause()}),this.canvas.addEventListener("pointermove",this.handlePointerMove),this.canvas.addEventListener("pointerleave",this.handlePointerLeave),document.addEventListener("fullscreenchange",this.handleFullscreenChange),this.player=this.createPlayer(),this.updateTimeLabels(),this.updatePlayPauseButton(),this.updateFullscreenButton(),this.refresh(),this.loadLocaleMessages()}connectedCallback(){const e=this.getAttribute("src");e&&this.loadFromSrc(e)}disconnectedCallback(){document.removeEventListener("fullscreenchange",this.handleFullscreenChange)}attributeChangedCallback(e,t,n){e==="src"&&n&&n!==t&&this.isConnected&&this.loadFromSrc(n)}async loadFromSrc(e){const t=await fetch(e);this.sightingData=await t.json()}get sightingData(){return tf(this.currentSighting)}set sightingData(e){this.player.stop(),this.currentSighting=nf(e),this.player=this.createPlayer(),this.updateTimeLabels(),this.updatePlayPauseButton(),this.refresh()}get sighting(){return this.currentSighting}get canvasElement(){return this.canvas}get renderer(){return this.canvasRenderer}get currentTime(){return this.player.time}set currentTime(e){this.player.seek(e)}get seekableDuration(){return this.player.seekableDuration}get autoReplayEnabled(){return this.loopEnabled}get positionLabel(){return this.timeStartLabel.textContent??""}get durationLabel(){return this.timeEndLabel.textContent??""}set showToolbar(e){this.toolbar.classList.toggle("hidden",!e)}get playbackState(){return this.player.playbackState}get selectedSourceIds(){return this.highlightedSourceIds}get durationSeconds(){return this.currentSighting.event.durationSeconds}set durationSeconds(e){this.currentSighting.event.durationSeconds=e,this.updateTimeLabels(),this.refresh(),this.updatePlayPauseButton()}set selectedSourceIds(e){const t=new Set(e);t.size===this.highlightedSourceIds.size&&[...t].every(s=>this.highlightedSourceIds.has(s))||(this.highlightedSourceIds=t,this.refresh())}setOccludedSourceIds(e){e.size===this.occludedSourceIds.size&&[...e].every(n=>this.occludedSourceIds.has(n))||(this.occludedSourceIds=e,this.refresh())}hasVisibleShapeAt(e,t){return this.currentSighting.timeline.hitTest(this.currentTime,e,t,this.occludedSourceIds)!==void 0}refresh(){this.seekInput.max=String(this.player.seekableDuration),this.player.seek(this.player.time)}canvasPointFromEvent(e){const t=this.canvas.getBoundingClientRect();if(!(t.width===0||t.height===0))return{x:(e.clientX-t.left)/t.width*this.canvas.width,y:(e.clientY-t.top)/t.height*this.canvas.height}}onFrame(e,t){this.canvasRenderer.clear(this.canvas.width,this.canvas.height);const n=this.playbackState!=="playing"?this.highlightedSourceIds:Kc;for(const[s,r]of t){if(this.occludedSourceIds.has(s))continue;const a=n.has(s);a&&n.size===1?this.canvasRenderer.paintShape({...r,selected:!0}):(this.canvasRenderer.paintShape(r),a&&this.canvasRenderer.paintMemberOutline(r))}if(n.size>1){const s=Ke.groupBoundsFor([...t].filter(([r])=>n.has(r)).map(([,r])=>r.bounds));this.canvasRenderer.paintGroupHandles(s)}this.seekInput.value=String(e),this.timeStartLabel.textContent=this.formatPosition(e),this.updatePlayPauseButton(),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{time:e}}))}createPlayer(){const e=new K0(this.currentSighting.timeline,(t,n)=>this.onFrame(t,n));return e.loop=this.loopEnabled,e}togglePlayPause(){this.player.seekableDuration<=0||(this.player.playbackState==="playing"?(this.player.pause(),this.refresh()):this.player.play(),this.updatePlayPauseButton())}updatePlayPauseButton(){const e=this.player.playbackState==="playing";this.playPauseButton.textContent=e?"⏸":"▶";const t=this.player.seekableDuration>0;this.playPauseButton.disabled=!t;const n=t?e?this.messages.pause:this.messages.play:this.messages.noDuration;this.playPauseButton.title=n,this.playPauseButton.setAttribute("aria-label",n),this.toolbar.classList.toggle("auto-hide",e),this.fullscreenButton.classList.toggle("auto-hide",e)}toggleLoop(){this.loopEnabled=!this.loopEnabled,this.loopButton.setAttribute("aria-pressed",String(this.loopEnabled)),this.player.loop=this.loopEnabled}toggleFullscreen(){document.fullscreenElement?document.exitFullscreen():this.fullscreenTarget.requestFullscreen().catch(e=>{console.error("<rr0-ufo>: requestFullscreen() failed —",e)})}updateFullscreenButton(){const e=document.fullscreenElement===this.fullscreenTarget;this.fullscreenButton.title=e?this.messages.exitFullscreen:this.messages.fullscreen,this.fullscreenButton.setAttribute("aria-label",this.fullscreenButton.title)}async loadLocaleMessages(){const e=Aa(navigator.languages,Zd);e!=="en"&&this.applyMessages(await of(e))}applyMessages(e){this.messages=e,this.timeStartLabel.title=e.currentPosition,this.timeEndLabel.title=e.duration,this.loopButton.title=e.autoReplay,this.loopButton.setAttribute("aria-label",e.autoReplay),this.updatePlayPauseButton(),this.updateFullscreenButton()}updateTimeLabels(){const e=this.currentSighting.event,t=lc(e);this.realDurationMs=t!==void 0&&t>0?t:void 0,this.realStartMs=e.time?Oo(e.time):void 0;const n=this.currentSighting.timeline.duration;this.player.playbackRate=this.realDurationMs!==void 0&&n>0?n/this.realDurationMs:1,this.player.durationOverrideMs=n>0?0:this.realDurationMs??0,this.timeEndLabel.textContent=this.formatEndOfTimeline(),this.timeStartLabel.textContent=this.formatPosition(this.player.time)}formatPosition(e){if(this.realDurationMs===void 0)return dr(e);const t=this.currentSighting.timeline.duration,n=t>0&&e<=t?e/t*this.realDurationMs:e;return this.realStartMs!==void 0?Jc(Zc(this.realStartMs+n)):dr(n)}formatEndOfTimeline(){return this.realDurationMs===void 0?dr(this.currentSighting.timeline.duration):this.realStartMs!==void 0?Jc(Zc(this.realStartMs+this.realDurationMs)):dr(this.realDurationMs)}}function Zc(i){const e=new Date(i);return{hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds()}}function Jc(i){if(i.hour===void 0)return"0:00";const e=t=>String(t).padStart(2,"0");return i.second?`${e(i.hour)}:${e(i.minute??0)}:${e(i.second)}`:`${e(i.hour)}:${e(i.minute??0)}`}function dr(i){const e=Math.round(i/1e3),t=Math.floor(e/60),n=e%60;return`${t}:${String(n).padStart(2,"0")}`}const Go="rr0-ufo";function Ia(){customElements.get(Go)||customElements.define(Go,df)}const uf=`
698
+ `;class fs{static MOON_ANGULAR_WIDTH_DEG=.5237;static angularWidthDeg(e){return 2*Math.atan(e.sizeM/(2*e.distanceM))*180/Math.PI}static widthPx(e,t,n){return t*e.sizeM/(2*e.distanceM*Math.tan(n*Math.PI/360))}static pxToDeg(e,t,n){return 2*Math.atan(e*Math.tan(n*Math.PI/360)/t)*180/Math.PI}static inMoons(e){return e/fs.MOON_ANGULAR_WIDTH_DEG}static lerp(e,t,n){if(!(!e||!t))return{sizeM:e.sizeM+(t.sizeM-e.sizeM)*n,distanceM:e.distanceM+(t.distanceM-e.distanceM)*n}}}function I0(i,e="#39ff14"){return{kind:"oval",bounds:i,color:e,angle:0,transparency:0,haloScale:0,selected:!1}}function R0(i,e,t="#39ff14"){return{kind:"polygon",bounds:i,points:e,color:t,angle:0,transparency:0,haloScale:0,selected:!1}}function C0(i,e="#39ff14"){const{width:t,height:n}=i;return R0(i,[{x:0,y:0},{x:t,y:0},{x:t,y:n},{x:0,y:n}],e)}const D0={oval:I0,polygon:C0};function Gc(i,e){return{...D0[e.presetId](i,e.color),transparency:e.transparency,haloScale:e.haloScale}}function P0(i,e,t){const{bounds:n}=i;return e>=n.x&&e<=n.x+n.width&&t>=n.y&&t<=n.y+n.height}function Yd(i,e,t){const n=i.bounds.width,s=i.bounds.height;return{...i,bounds:{x:e-n/2,y:t-s/2,width:n,height:s}}}function Qt(i,e,t){return i+(e-i)*t}function Hc(i){const e=/^#([0-9a-f]{6})$/i.exec(i);if(!e)return;const t=parseInt(e[1],16);return[t>>16&255,t>>8&255,t&255]}function L0(i){return`#${i.map(e=>Math.round(e).toString(16).padStart(2,"0")).join("")}`}function N0(i,e,t){const n=Hc(i),s=Hc(e);return!n||!s?t<1?i:e:L0([Qt(n[0],s[0],t),Qt(n[1],s[1],t),Qt(n[2],s[2],t)])}function U0(i,e,t){const n={x:Qt(i.bounds.x,e.bounds.x,t),y:Qt(i.bounds.y,e.bounds.y,t),width:Qt(i.bounds.width,e.bounds.width,t),height:Qt(i.bounds.height,e.bounds.height,t)},s=Qt(i.angle,e.angle,t),r=Qt(i.transparency,e.transparency,t),a=Qt(i.haloScale,e.haloScale,t),o=N0(i.color,e.color,t),l=fs.lerp(i.physical,e.physical,t);return i.kind==="polygon"&&e.kind==="polygon"&&i.points.length===e.points.length?{...i,bounds:n,angle:s,transparency:r,haloScale:a,color:o,physical:l,points:i.points.map((c,h)=>({x:Qt(c.x,e.points[h].x,t),y:Qt(c.y,e.points[h].y,t)}))}:{...t<1?i:e,bounds:n,angle:s,transparency:r,haloScale:a,color:o,physical:l}}class wa{keyframes=[];order=[];groups=[];addKeyframe(e,t){const n=this.findInsertIndex(e);if(this.keyframes[n]?.t===e){const s=new Set(t.map(r=>r.sourceId));this.keyframes[n]={t:e,shapes:[...this.keyframes[n].shapes.filter(r=>!s.has(r.sourceId)),...t]}}else this.keyframes.splice(n,0,{t:e,shapes:[...t]});for(const s of t)this.order.includes(s.sourceId)||this.order.push(s.sourceId)}findInsertIndex(e){let t=0,n=this.keyframes.length;for(;t<n;){const s=t+n>>>1;this.keyframes[s].t<e?t=s+1:n=s}return t}getKeyframeAt(e){const t=this.findInsertIndex(e),n=this.keyframes[t];return n?.t===e?n:void 0}getShapeAt(e,t){return this.getKeyframeAt(e)?.shapes.find(n=>n.sourceId===t)?.shape}getLatestShapeAt(e,t){let n=this.findInsertIndex(e);for(this.keyframes[n]?.t!==e&&(n-=1);n>=0;n--){const s=this.keyframes[n].shapes.find(r=>r.sourceId===t);if(s)return s.shape}}getInterpolatedShapeAt(e,t){const n=this.findShapeAtOrBefore(e,t);if(n?.t===e)return n.shape;const s=this.findShapeAtOrAfter(e,t);return n?s?U0(n.shape,s.shape,(e-n.t)/(s.t-n.t)):n.shape:s?.shape}findShapeAtOrBefore(e,t){let n=this.findInsertIndex(e);for(this.keyframes[n]?.t!==e&&(n-=1);n>=0;n--){const s=this.keyframes[n].shapes.find(r=>r.sourceId===t);if(s)return{t:this.keyframes[n].t,shape:s.shape}}}findShapeAtOrAfter(e,t){for(let n=this.findInsertIndex(e);n<this.keyframes.length;n++){const s=this.keyframes[n].shapes.find(r=>r.sourceId===t);if(s)return{t:this.keyframes[n].t,shape:s.shape}}}hitTest(e,t,n,s){const r=this.sourceIds;for(let a=r.length-1;a>=0;a--){if(s?.has(r[a]))continue;const o=this.getInterpolatedShapeAt(e,r[a]);if(o&&P0(o,t,n))return{sourceId:r[a],shape:o}}}removeSource(e){for(let t=this.keyframes.length-1;t>=0;t--){const n=this.keyframes[t].shapes.filter(s=>s.sourceId!==e);n.length===0?this.keyframes.splice(t,1):n.length!==this.keyframes[t].shapes.length&&(this.keyframes[t]={t:this.keyframes[t].t,shapes:n})}this.order=this.order.filter(t=>t!==e),this.removeFromGroup(e)}group(e){if(!(e.length<2)){for(const t of e)this.removeFromGroup(t);this.groups.push([...e])}}ungroup(e){const t=this.groups.findIndex(n=>n.includes(e));t!==-1&&this.groups.splice(t,1)}groupMembers(e){return this.groups.find(t=>t.includes(e))}removeFromGroup(e){const t=this.groups.findIndex(s=>s.includes(e));if(t===-1)return;const n=this.groups[t].filter(s=>s!==e);n.length<2?this.groups.splice(t,1):this.groups[t]=n}bringToFront(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.push(e))}sendToBack(e){const t=this.order.indexOf(e);t!==-1&&(this.order.splice(t,1),this.order.unshift(e))}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get sourceIds(){return[...this.order]}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes,order:this.order,groups:this.groups}}static fromJSON(e){const t=new wa;for(const n of e.keyframes)t.addKeyframe(n.t,n.shapes);if(e.order){const n=t.order.filter(s=>!e.order.includes(s));t.order=[...e.order,...n]}return e.groups&&(t.groups=e.groups.map(n=>n.filter(s=>t.order.includes(s))).filter(n=>n.length>=2)),t}}function F0(i,e,t){return Math.max(e,Math.min(t,i))}function O0(i,e,t){const n=((e-i)%360+540)%360-180;return((i+n*t)%360+360)%360}function Ts(i,e,t){return i+(e-i)*t}function B0(i,e,t){return{lat:i.lat===void 0||e.lat===void 0?void 0:Ts(i.lat,e.lat,t),lng:i.lng===void 0||e.lng===void 0?void 0:Ts(i.lng,e.lng,t),elevationM:Ts(i.elevationM,e.elevationM,t),headingDeg:i.headingDeg===void 0||e.headingDeg===void 0?void 0:O0(i.headingDeg,e.headingDeg,t),pitchDeg:Ts(i.pitchDeg,e.pitchDeg,t),fovDeg:Ts(i.fovDeg,e.fovDeg,t)}}class Js{keyframes=[];addKeyframe(e,t){const n=this.findInsertIndex(e);this.keyframes[n]?.t===e?this.keyframes[n]={t:e,pose:t}:this.keyframes.splice(n,0,{t:e,pose:t})}clear(){this.keyframes.length=0}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}findInsertIndex(e){let t=0,n=this.keyframes.length;for(;t<n;){const s=t+n>>>1;this.keyframes[s].t<e?t=s+1:n=s}return t}getLatestPoseAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].pose:void 0}getInterpolatedPoseAt(e){const t=this.findInsertIndex(e),n=this.keyframes[t]?.t===e?this.keyframes[t]:this.keyframes[t-1];if(n?.t===e)return n.pose;const s=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return n?s?B0(n.pose,s.pose,F0((e-n.t)/(s.t-n.t),0,1)):n.pose:s?.pose}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes}}static fromJSON(e){const t=new Js;for(const n of e.keyframes)t.addKeyframe(n.t,n.pose);return t}}function cr(i,e,t){return i+(e-i)*t}function z0(i,e,t){const n=((e-i)%360+540)%360-180;return((i+n*t)%360+360)%360}function k0(i,e,t){return Math.max(e,Math.min(t,i))}function G0(i,e,t){return{cloudCover:cr(i.cloudCover,e.cloudCover,t),cloudDarkness:cr(i.cloudDarkness,e.cloudDarkness,t),precipitationType:t<1?i.precipitationType:e.precipitationType,precipitationIntensity:cr(i.precipitationIntensity,e.precipitationIntensity,t),windDirectionDeg:z0(i.windDirectionDeg,e.windDirectionDeg,t),windSpeed:cr(i.windSpeed,e.windSpeed,t),storm:t<1?i.storm:e.storm}}class Qs{keyframes=[];addKeyframe(e,t){const n=this.findInsertIndex(e);this.keyframes[n]?.t===e?this.keyframes[n]={t:e,weather:t}:this.keyframes.splice(n,0,{t:e,weather:t})}clear(){this.keyframes.length=0}removeKeyframeAt(e){const t=this.findInsertIndex(e);this.keyframes[t]?.t===e&&this.keyframes.splice(t,1)}findInsertIndex(e){let t=0,n=this.keyframes.length;for(;t<n;){const s=t+n>>>1;this.keyframes[s].t<e?t=s+1:n=s}return t}getLatestWeatherAt(e){let t=this.findInsertIndex(e);return this.keyframes[t]?.t!==e&&(t-=1),t>=0?this.keyframes[t].weather:void 0}getInterpolatedWeatherAt(e){const t=this.findInsertIndex(e),n=this.keyframes[t]?.t===e?this.keyframes[t]:this.keyframes[t-1];if(n?.t===e)return n.weather;const s=this.keyframes[t]?.t===e?void 0:this.keyframes[t];return n?s?G0(n.weather,s.weather,k0((e-n.t)/(s.t-n.t),0,1)):n.weather:s?.weather}get duration(){return this.keyframes.length===0?0:this.keyframes[this.keyframes.length-1].t}get allKeyframes(){return this.keyframes}toJSON(){return{keyframes:this.keyframes}}static fromJSON(e){const t=new Qs;for(const n of e.keyframes)t.addKeyframe(n.t,n.weather);return t}}const qd={cloudCover:0,cloudDarkness:0,precipitationType:"none",precipitationIntensity:0,windDirectionDeg:0,windSpeed:0,storm:!1},H0=/^(?:(?<year>\d{4}|\d{3}X|\d{2}XX)(-(?<month>0[1-9]|1[0-2]))?(-(?<day>0[1-9]|[12]\d|3[01]))?(?:[T ](?<hour>[01]?\d|2[0-3]):(?<minute>[0-5]\d)(?::(?<second>[0-5]\d))?)?|(?<timeOnlyHour>[01]?\d|2[0-3]):(?<timeOnlyMinute>[0-5]\d)(?::(?<timeOnlySecond>[0-5]\d))?)[?~%]?$/;function Vc(i){const e=H0.exec(i.trim());if(!e?.groups)return;const{year:t,month:n,day:s,hour:r,minute:a,second:o,timeOnlyHour:l,timeOnlyMinute:c,timeOnlySecond:h}=e.groups,u=r??l,d=a??c,p=o??h;return{year:t&&!t.includes("X")?Number(t):void 0,month:n?Number(n):void 0,day:s?Number(s):void 0,hour:u?Number(u):void 0,minute:d?Number(d):void 0,second:p?Number(p):void 0,raw:i.trim()}}function Wc(i){if(i.raw!==void 0)return i.raw;if(i.year===void 0){if(i.hour===void 0)return"";let t=`${String(i.hour).padStart(2,"0")}:${String(i.minute??0).padStart(2,"0")}`;return i.second!==void 0&&(t+=`:${String(i.second).padStart(2,"0")}`),t}let e=String(i.year).padStart(4,"0");return i.month===void 0||(e+=`-${String(i.month).padStart(2,"0")}`,i.day===void 0)||(e+=`-${String(i.day).padStart(2,"0")}`,i.hour===void 0)||(e+=`T${String(i.hour).padStart(2,"0")}:${String(i.minute??0).padStart(2,"0")}`,i.second!==void 0&&(e+=`:${String(i.second).padStart(2,"0")}`)),e}function Oo(i){if(i.year!==void 0)return Date.UTC(i.year,(i.month??1)-1,i.day??1,i.hour??0,i.minute??0,i.second??0)}function Xc(i){return(((i.day??0)*24+(i.hour??0))*60+(i.minute??0))*6e4+(i.second??0)*1e3}const V0=["year","month","day","hour","minute"],W0=["day","hour","minute"];function Yc(i,e){return e.filter(t=>i[t]!==void 0).join(",")}function lc(i){if(i.durationSeconds!==void 0)return i.durationSeconds*1e3;if(!i.time||!i.endTime)return;const e=i.time.year!==void 0&&i.endTime.year!==void 0,t=e?V0:W0,n=Yc(i.time,t);if(!(n===""||n!==Yc(i.endTime,t)))return e?Oo(i.endTime)-Oo(i.time):Xc(i.endTime)-Xc(i.time)}function X0(i){if(i.durationSeconds===void 0&&!(!i.time||!i.endTime))return lc(i)===void 0?"imprecise":void 0}class Ta{constructor(e,t,n,s,r,a,o,l=[]){this.event=e,this.timeline=t,this.witnessTrack=n,this.weatherTrack=s,this.witness=r,this.caseId=a,this.weather=o,this.decor=l}static create(e,t,n){return new Ta({eventType:"sighting",time:e,place:t},new wa,new Js,new Qs,n)}}const Y0=0,q0=0,$0=60;function Bo(i,e){const t=i.witnessTrack.getInterpolatedPoseAt(e);if(t)return t;const n=i.event.place?.[0];if(n)return{lat:n.lat,lng:n.lng,elevationM:Y0,headingDeg:void 0,pitchDeg:q0,fovDeg:$0}}function zo(i,e){return i.weatherTrack.getInterpolatedWeatherAt(e)??i.weather??qd}class K0{constructor(e,t){this.timeline=e,this.onFrame=t}rafId=null;state="stopped";currentT=0;lastWallTime=0;playbackRate=1;loop=!1;durationOverrideMs=0;get seekableDuration(){return Math.max(this.timeline.duration,this.durationOverrideMs)}play(){if(this.state==="playing")return;this.currentT>=this.seekableDuration&&(this.currentT=0,this.resolveFrame(0)),this.state="playing",this.lastWallTime=performance.now();const e=()=>{if(this.state!=="playing")return;const t=performance.now();if(this.currentT+=(t-this.lastWallTime)*this.playbackRate,this.lastWallTime=t,this.currentT>=this.seekableDuration){if(this.loop&&this.seekableDuration>0){this.currentT%=this.seekableDuration,this.resolveFrame(this.currentT),this.rafId=requestAnimationFrame(e);return}this.currentT=this.seekableDuration,this.stop(),this.resolveFrame(this.currentT);return}this.resolveFrame(this.currentT),this.rafId=requestAnimationFrame(e)};this.rafId=requestAnimationFrame(e)}pause(){this.state==="playing"&&(this.state="paused",this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null))}stop(){this.state="stopped",this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}seek(e){this.currentT=Math.max(0,Math.min(e,this.seekableDuration)),this.resolveFrame(this.currentT)}get playbackState(){return this.state}get time(){return this.currentT}resolveFrame(e){const t=new Map;for(const n of this.timeline.sourceIds){const s=this.timeline.getInterpolatedShapeAt(e,n);s&&t.set(n,s)}this.onFrame(e,t)}}const $d=["nw","n","ne","e","se","s","sw","w"],Z0=24,ri=8,Kd=3,J0=8;function Hs(i,e,t){const n=i.x-e.x,s=i.y-e.y,r=Math.cos(t),a=Math.sin(t);return{x:e.x+n*r-s*a,y:e.y+n*a+s*r}}function Si(i){return{x:i.x+i.width/2,y:i.y+i.height/2}}const Q0={nw:{left:!0,top:!0},n:{top:!0},ne:{right:!0,top:!0},e:{right:!0},se:{right:!0,bottom:!0},s:{bottom:!0},sw:{left:!0,bottom:!0},w:{left:!0}};class Ke{static handlePointsFor(e){const{x:t,y:n,width:s,height:r}=e.bounds,a={nw:{x:t,y:n},n:{x:t+s/2,y:n},ne:{x:t+s,y:n},e:{x:t+s,y:n+r/2},se:{x:t+s,y:n+r},s:{x:t+s/2,y:n+r},sw:{x:t,y:n+r},w:{x:t,y:n+r/2},rotate:{x:t+s/2,y:n-Z0}},o=Si(e.bounds),l={};for(const c of Object.keys(a))l[c]=Hs(a[c],o,e.angle);return l}static hitTestHandle(e,t,n=8,s=[...$d,"rotate"]){const r=Ke.handlePointsFor(e);for(const a of s)if(Math.hypot(t.x-r[a].x,t.y-r[a].y)<=n)return a}static HANDLE_DIRECTION_DEG={e:0,se:45,s:90,sw:135,w:180,nw:225,n:270,ne:315};static resizeAxisFor(e,t){const s=((Ke.HANDLE_DIRECTION_DEG[e]+t*180/Math.PI)%180+180)%180/45,r=["ew","nwse","ns","nesw"];return r[Math.round(s)%r.length]}static resizeBounds(e,t,n,s){const r=Si(e),a=Hs(s,r,-t),{x:o,y:l,width:c,height:h}=e,u=Q0[n];let d=o,p=l,v=o+c,y=l+h;return u.left&&(d=Math.min(a.x,v-ri)),u.right&&(v=Math.max(a.x,d+ri)),u.top&&(p=Math.min(a.y,y-ri)),u.bottom&&(y=Math.max(a.y,p+ri)),{x:d,y:p,width:v-d,height:y-p}}static resizeShape(e,t,n){if(t==="rotate")throw new Error("resizeShape does not accept the rotate handle");const{width:s,height:r}=e.bounds,a=Ke.resizeBounds(e.bounds,e.angle,t,n);if(e.kind==="oval")return{...e,bounds:a};const o=s===0?1:a.width/s,l=r===0?1:a.height/r;return{...e,bounds:a,points:e.points.map(c=>({x:c.x*o,y:c.y*l}))}}static rotateShape(e,t){const n=Si(e.bounds),s=Math.atan2(t.y-n.y,t.x-n.x)+Math.PI/2;return{...e,angle:s}}static groupBoundsFor(e){const t=Math.min(...e.map(a=>a.x)),n=Math.min(...e.map(a=>a.y)),s=Math.max(...e.map(a=>a.x+a.width)),r=Math.max(...e.map(a=>a.y+a.height));return{x:t,y:n,width:s-t,height:r-n}}static vertexPointsFor(e){const t=Si(e.bounds);return e.points.map(n=>Hs({x:e.bounds.x+n.x,y:e.bounds.y+n.y},t,e.angle))}static hitTestVertex(e,t,n=J0){const s=Ke.vertexPointsFor(e);let r,a=n;return s.forEach((o,l)=>{const c=Math.hypot(t.x-o.x,t.y-o.y);c<=a&&(a=c,r=l)}),r}static toLocalPoint(e,t){const n=Si(e.bounds),s=Hs(t,n,-e.angle);return{x:s.x-e.bounds.x,y:s.y-e.bounds.y}}static moveVertex(e,t,n){const s=e.points.map((u,d)=>d===t?Ke.toLocalPoint(e,n):u),r=Math.min(...s.map(u=>u.x)),a=Math.min(...s.map(u=>u.y)),o=Math.max(...s.map(u=>u.x)),l=Math.max(...s.map(u=>u.y)),c={x:e.bounds.x+r,y:e.bounds.y+a,width:o-r,height:l-a},h=s.map(u=>({x:u.x-r,y:u.y-a}));return{...e,bounds:c,points:h}}static insertVertexNear(e,t){const n=Ke.toLocalPoint(e,t),{points:s}=e;let r=s.length-1,a=1/0;for(let l=0;l<s.length;l++){const c=s[l],h=s[(l+1)%s.length],u=Ke.distanceToSegment(n,c,h);u<a&&(a=u,r=l)}const o=[...s.slice(0,r+1),n,...s.slice(r+1)];return{...e,points:o}}static distanceToSegment(e,t,n){const s=n.x-t.x,r=n.y-t.y,a=s*s+r*r,o=a===0?0:Math.max(0,Math.min(1,((e.x-t.x)*s+(e.y-t.y)*r)/a)),l=t.x+o*s,c=t.y+o*r;return Math.hypot(e.x-l,e.y-c)}static deleteVertex(e,t){return e.points.length<=Kd?e:{...e,points:e.points.filter((n,s)=>s!==t)}}}class Ba{constructor(e){this.members=e}bounds(){return Ke.groupBoundsFor(this.members.map(e=>e.shape.bounds))}scaleTo(e){const t=this.bounds(),n=t.width===0?1:e.width/t.width,s=t.height===0?1:e.height/t.height;return this.members.map(({sourceId:r,shape:a})=>{const o=Math.max(a.bounds.width*n,ri),l=Math.max(a.bounds.height*s,ri),c=e.x+(a.bounds.x-t.x)*n,h=e.y+(a.bounds.y-t.y)*s,u={x:c,y:h,width:o,height:l};return a.kind==="oval"?{sourceId:r,shape:{...a,bounds:u}}:{sourceId:r,shape:{...a,bounds:u,points:a.points.map(d=>({x:d.x*n,y:d.y*s}))}}})}resize(e,t){return this.scaleTo(Ke.resizeBounds(this.bounds(),0,e,t))}rotate(e,t){const n=Si(this.bounds()),s=a=>Math.atan2(a.y-n.y,a.x-n.x),r=s(e)-s(t);return this.members.map(({sourceId:a,shape:o})=>{const l=Si(o.bounds),c=Hs(l,n,r),h={x:c.x-o.bounds.width/2,y:c.y-o.bounds.height/2,width:o.bounds.width,height:o.bounds.height};return{sourceId:a,shape:{...o,bounds:h,angle:o.angle+r}}})}}const ko=6,hr=ko/2,j0=20,qc=4;class ef{constructor(e){this.ctx=e}clear(e,t){this.ctx.clearRect(0,0,e,t)}paintShape(e){this.ctx.save(),this.ctx.globalAlpha=1-e.transparency,e.haloScale>0&&this.paintHalo(e),this.paintBase(e),this.ctx.restore(),e.selected&&this.paintSelectionHandles(e)}paintBase(e){if(this.ctx.fillStyle=e.color,this.ctx.beginPath(),e.kind==="oval"){const{x:t,y:n,width:s,height:r}=e.bounds,a=s/2,o=r/2;this.ctx.ellipse(t+a,n+o,a,o,e.angle,0,2*Math.PI)}else{const{x:t,y:n,width:s,height:r}=e.bounds;this.ctx.save(),this.ctx.translate(t+s/2,n+r/2),this.ctx.rotate(e.angle),this.ctx.translate(-s/2,-r/2),e.points.forEach((a,o)=>{o===0?this.ctx.moveTo(a.x,a.y):this.ctx.lineTo(a.x,a.y)}),this.ctx.closePath(),this.ctx.restore()}this.ctx.fill()}paintHalo(e){this.ctx.save(),this.ctx.shadowColor=e.color,this.ctx.shadowBlur=j0*e.haloScale,this.paintBase(e),this.ctx.restore()}paintSelectionHandles(e){this.paintHandleFrame(Ke.handlePointsFor(e),{includeRotate:!0}),e.kind==="polygon"&&this.paintVertexHandles(e)}paintVertexHandles(e){this.ctx.fillStyle="#39f";for(const t of Ke.vertexPointsFor(e))this.ctx.beginPath(),this.ctx.ellipse(t.x,t.y,qc,qc,0,0,2*Math.PI),this.ctx.fill()}paintMemberOutline(e){this.paintOutline(Ke.handlePointsFor(e))}paintGroupHandles(e){this.paintHandleFrame(Ke.handlePointsFor({bounds:e,angle:0}),{includeRotate:!0})}paintOutline(e){this.ctx.strokeStyle="lightgray",this.ctx.beginPath(),this.ctx.moveTo(e.nw.x,e.nw.y),this.ctx.lineTo(e.ne.x,e.ne.y),this.ctx.lineTo(e.se.x,e.se.y),this.ctx.lineTo(e.sw.x,e.sw.y),this.ctx.closePath(),this.ctx.stroke()}paintHandleFrame(e,{includeRotate:t}){this.paintOutline(e),this.ctx.fillStyle="lightgray";for(const n of $d){const s=e[n];this.ctx.fillRect(s.x-hr,s.y-hr,ko,ko)}t&&(this.ctx.beginPath(),this.ctx.moveTo(e.n.x,e.n.y),this.ctx.lineTo(e.rotate.x,e.rotate.y),this.ctx.stroke(),this.ctx.beginPath(),this.ctx.ellipse(e.rotate.x,e.rotate.y,hr+1,hr+1,0,0,2*Math.PI),this.ctx.fill())}}function tf(i){return{version:1,time:i.event.time,endTime:i.event.endTime,durationSeconds:i.event.durationSeconds,utcOffsetHours:i.event.utcOffsetHours,place:i.event.place,witness:i.witness,caseId:i.caseId,description:i.event.description,tags:i.event.tags,timeline:i.timeline.toJSON(),witnessTrack:i.witnessTrack.toJSON(),weatherTrack:i.weatherTrack.toJSON(),weather:i.weather,decor:i.decor}}function nf(i){return new Ta({eventType:"sighting",time:i.time,endTime:i.endTime,durationSeconds:i.durationSeconds,utcOffsetHours:i.utcOffsetHours,place:i.place,description:i.description,tags:i.tags},wa.fromJSON(i.timeline),i.witnessTrack?Js.fromJSON(i.witnessTrack):new Js,i.weatherTrack?Qs.fromJSON(i.weatherTrack):new Qs,i.witness,i.caseId,i.weather,i.decor??[])}function Aa(i,e){for(const t of i){const n=t.toLowerCase().split("-")[0];if(e.includes(n))return n}return"en"}const sf="modulepreload",rf=function(i,e){return new URL(i,e).href},$c={},ha=function(e,t,n){let s=Promise.resolve();if(t&&t.length>0){let a=function(h){return Promise.all(h.map(u=>Promise.resolve(u).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const o=document.getElementsByTagName("link"),l=document.querySelector("meta[property=csp-nonce]"),c=l?.nonce||l?.getAttribute("nonce");s=a(t.map(h=>{if(h=rf(h,n),h in $c)return;$c[h]=!0;const u=h.endsWith(".css"),d=u?'[rel="stylesheet"]':"";if(!!n)for(let y=o.length-1;y>=0;y--){const m=o[y];if(m.href===h&&(!u||m.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${h}"]${d}`))return;const v=document.createElement("link");if(v.rel=u?"stylesheet":sf,u||(v.as="script"),v.crossOrigin="",v.href=h,c&&v.setAttribute("nonce",c),document.head.appendChild(v),u)return new Promise((y,m)=>{v.addEventListener("load",y),v.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${h}`)))})}))}function r(a){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=a,window.dispatchEvent(o),!o.defaultPrevented)throw a}return s.then(a=>{for(const o of a||[])o.status==="rejected"&&r(o.reason);return e().catch(r)})},Zd=["en","fr"],af={en:()=>ha(()=>Promise.resolve().then(()=>hf),void 0,import.meta.url).then(i=>i.ufoMessages_en),fr:()=>ha(()=>import("./UfoMessages_fr-NZrwNFCQ.js"),[],import.meta.url).then(i=>i.ufoMessages_fr)};function of(i){return af[i]()}const lf={en:()=>ha(()=>Promise.resolve().then(()=>oy),void 0,import.meta.url).then(i=>i.ufoRecorderMessages_en),fr:()=>ha(()=>import("./UfoRecorderMessages_fr-CsLFK8nn.js"),[],import.meta.url).then(i=>i.ufoRecorderMessages_fr)};function cf(i){return lf[i]()}const Jd={play:"Play",pause:"Pause",noDuration:"No observation duration",autoReplay:"Auto-replay",currentPosition:"Current position",duration:"Duration",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen"},hf=Object.freeze(Object.defineProperty({__proto__:null,ufoMessages_en:Jd},Symbol.toStringTag,{value:"Module"})),Kc=new Set;class df extends HTMLElement{static get observedAttributes(){return["src"]}shadow;stageElement;canvas;canvasRenderer;tooltip;toolbar;playPauseButton;loopButton;fullscreenButton;seekInput;timeStartLabel;timeEndLabel;currentSighting=Ta.create();player;loopEnabled=!0;highlightedSourceIds=new Set;occludedSourceIds=Kc;enableClickToPlay=!0;fullscreenTarget;messages=Jd;realDurationMs;realStartMs;handleFullscreenChange=()=>this.updateFullscreenButton();handlePointerMove=e=>{const t=this.canvasPointFromEvent(e),n=t&&this.currentSighting.timeline.hitTest(this.currentTime,t.x,t.y,this.occludedSourceIds);if(!n?.shape.title){this.tooltip.hidden=!0;return}this.tooltip.textContent=n.shape.title,this.tooltip.hidden=!1;const s=this.stageElement.getBoundingClientRect();this.tooltip.style.left=`${e.clientX-s.left+12}px`,this.tooltip.style.top=`${e.clientY-s.top+12}px`};handlePointerLeave=()=>{this.tooltip.hidden=!0};constructor(){super(),this.shadow=this.attachShadow({mode:"open"});const e=document.createElement("template");e.innerHTML=`<style>${A0}</style>${T0}`,this.shadow.appendChild(e.content.cloneNode(!0)),this.stageElement=this.shadow.getElementById("stage"),this.canvas=this.shadow.getElementById("canvas"),this.canvasRenderer=new ef(this.canvas.getContext("2d")),this.tooltip=this.shadow.getElementById("tooltip"),this.toolbar=this.shadow.getElementById("toolbar"),this.playPauseButton=this.shadow.getElementById("play-pause"),this.loopButton=this.shadow.getElementById("loop"),this.fullscreenButton=this.shadow.getElementById("fullscreen"),this.seekInput=this.shadow.getElementById("seek"),this.timeStartLabel=this.shadow.getElementById("time-start"),this.timeEndLabel=this.shadow.getElementById("time-end"),this.fullscreenTarget=this.stageElement,this.playPauseButton.addEventListener("click",()=>this.togglePlayPause()),this.loopButton.addEventListener("click",()=>this.toggleLoop()),this.fullscreenButton.addEventListener("click",()=>this.toggleFullscreen()),this.seekInput.addEventListener("input",()=>this.player.seek(Number(this.seekInput.value))),this.canvas.addEventListener("click",()=>{this.enableClickToPlay&&this.togglePlayPause()}),this.canvas.addEventListener("pointermove",this.handlePointerMove),this.canvas.addEventListener("pointerleave",this.handlePointerLeave),document.addEventListener("fullscreenchange",this.handleFullscreenChange),this.player=this.createPlayer(),this.updateTimeLabels(),this.updatePlayPauseButton(),this.updateFullscreenButton(),this.refresh(),this.loadLocaleMessages()}connectedCallback(){const e=this.getAttribute("src");e&&this.loadFromSrc(e)}disconnectedCallback(){document.removeEventListener("fullscreenchange",this.handleFullscreenChange)}attributeChangedCallback(e,t,n){e==="src"&&n&&n!==t&&this.isConnected&&this.loadFromSrc(n)}async loadFromSrc(e){const t=await fetch(e);this.sightingData=await t.json()}get sightingData(){return tf(this.currentSighting)}set sightingData(e){this.player.stop(),this.currentSighting=nf(e),this.player=this.createPlayer(),this.updateTimeLabels(),this.updatePlayPauseButton(),this.refresh()}get sighting(){return this.currentSighting}get canvasElement(){return this.canvas}get renderer(){return this.canvasRenderer}get currentTime(){return this.player.time}set currentTime(e){this.player.seek(e)}get seekableDuration(){return this.player.seekableDuration}get autoReplayEnabled(){return this.loopEnabled}get positionLabel(){return this.timeStartLabel.textContent??""}get durationLabel(){return this.timeEndLabel.textContent??""}set showToolbar(e){this.toolbar.classList.toggle("hidden",!e)}get playbackState(){return this.player.playbackState}get selectedSourceIds(){return this.highlightedSourceIds}get durationSeconds(){return this.currentSighting.event.durationSeconds}set durationSeconds(e){this.currentSighting.event.durationSeconds=e,this.updateTimeLabels(),this.refresh(),this.updatePlayPauseButton()}set selectedSourceIds(e){const t=new Set(e);t.size===this.highlightedSourceIds.size&&[...t].every(s=>this.highlightedSourceIds.has(s))||(this.highlightedSourceIds=t,this.refresh())}setOccludedSourceIds(e){e.size===this.occludedSourceIds.size&&[...e].every(n=>this.occludedSourceIds.has(n))||(this.occludedSourceIds=e,this.refresh())}hasVisibleShapeAt(e,t){return this.currentSighting.timeline.hitTest(this.currentTime,e,t,this.occludedSourceIds)!==void 0}refresh(){this.seekInput.max=String(this.player.seekableDuration),this.player.seek(this.player.time)}canvasPointFromEvent(e){const t=this.canvas.getBoundingClientRect();if(!(t.width===0||t.height===0))return{x:(e.clientX-t.left)/t.width*this.canvas.width,y:(e.clientY-t.top)/t.height*this.canvas.height}}onFrame(e,t){this.canvasRenderer.clear(this.canvas.width,this.canvas.height);const n=this.playbackState!=="playing"?this.highlightedSourceIds:Kc;for(const[s,r]of t){if(this.occludedSourceIds.has(s))continue;const a=n.has(s);a&&n.size===1?this.canvasRenderer.paintShape({...r,selected:!0}):(this.canvasRenderer.paintShape(r),a&&this.canvasRenderer.paintMemberOutline(r))}if(n.size>1){const s=Ke.groupBoundsFor([...t].filter(([r])=>n.has(r)).map(([,r])=>r.bounds));this.canvasRenderer.paintGroupHandles(s)}this.seekInput.value=String(e),this.timeStartLabel.textContent=this.formatPosition(e),this.updatePlayPauseButton(),this.dispatchEvent(new CustomEvent("timeupdate",{detail:{time:e}}))}createPlayer(){const e=new K0(this.currentSighting.timeline,(t,n)=>this.onFrame(t,n));return e.loop=this.loopEnabled,e}togglePlayPause(){this.player.seekableDuration<=0||(this.player.playbackState==="playing"?(this.player.pause(),this.refresh()):this.player.play(),this.updatePlayPauseButton())}updatePlayPauseButton(){const e=this.player.playbackState==="playing";this.playPauseButton.textContent=e?"⏸":"▶";const t=this.player.seekableDuration>0;this.playPauseButton.disabled=!t;const n=t?e?this.messages.pause:this.messages.play:this.messages.noDuration;this.playPauseButton.title=n,this.playPauseButton.setAttribute("aria-label",n),this.toolbar.classList.toggle("auto-hide",e),this.fullscreenButton.classList.toggle("auto-hide",e)}toggleLoop(){this.loopEnabled=!this.loopEnabled,this.loopButton.setAttribute("aria-pressed",String(this.loopEnabled)),this.player.loop=this.loopEnabled}toggleFullscreen(){document.fullscreenElement?document.exitFullscreen():this.fullscreenTarget.requestFullscreen().catch(e=>{console.error("<rr0-ufo>: requestFullscreen() failed —",e)})}updateFullscreenButton(){const e=document.fullscreenElement===this.fullscreenTarget;this.fullscreenButton.title=e?this.messages.exitFullscreen:this.messages.fullscreen,this.fullscreenButton.setAttribute("aria-label",this.fullscreenButton.title)}async loadLocaleMessages(){const e=Aa(navigator.languages,Zd);e!=="en"&&this.applyMessages(await of(e))}applyMessages(e){this.messages=e,this.timeStartLabel.title=e.currentPosition,this.timeEndLabel.title=e.duration,this.loopButton.title=e.autoReplay,this.loopButton.setAttribute("aria-label",e.autoReplay),this.updatePlayPauseButton(),this.updateFullscreenButton()}updateTimeLabels(){const e=this.currentSighting.event,t=lc(e);this.realDurationMs=t!==void 0&&t>0?t:void 0,this.realStartMs=e.time?Oo(e.time):void 0;const n=this.currentSighting.timeline.duration;this.player.playbackRate=this.realDurationMs!==void 0&&n>0?n/this.realDurationMs:1,this.player.durationOverrideMs=n>0?0:this.realDurationMs??0,this.timeEndLabel.textContent=this.formatEndOfTimeline(),this.timeStartLabel.textContent=this.formatPosition(this.player.time)}formatPosition(e){if(this.realDurationMs===void 0)return dr(e);const t=this.currentSighting.timeline.duration,n=t>0&&e<=t?e/t*this.realDurationMs:e;return this.realStartMs!==void 0?Jc(Zc(this.realStartMs+n)):dr(n)}formatEndOfTimeline(){return this.realDurationMs===void 0?dr(this.currentSighting.timeline.duration):this.realStartMs!==void 0?Jc(Zc(this.realStartMs+this.realDurationMs)):dr(this.realDurationMs)}}function Zc(i){const e=new Date(i);return{hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds()}}function Jc(i){if(i.hour===void 0)return"0:00";const e=t=>String(t).padStart(2,"0");return i.second?`${e(i.hour)}:${e(i.minute??0)}:${e(i.second)}`:`${e(i.hour)}:${e(i.minute??0)}`}function dr(i){const e=Math.round(i/1e3),t=Math.floor(e/60),n=e%60;return`${t}:${String(n).padStart(2,"0")}`}const Go="rr0-ufo";function Ia(){customElements.get(Go)||customElements.define(Go,df)}const uf=`
695
699
  <div class="stage" id="stage">
696
700
  <div class="frame" id="frame">
697
701
  <canvas id="scene-canvas"></canvas>