@rr0/ufoathome 0.17.1 → 0.18.0

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
@@ -82,7 +82,8 @@ The lightweight component (~9KB): a canvas plus Play/Pause/Loop/seek controls. U
82
82
 
83
83
  Playback matches the observation's *real reported duration* when it's known: set `time`/`endTime`, or `time`/
84
84
  `durationSeconds`, in the [data format](#data-format) (`durationSeconds` takes precedence over `endTime` if both are
85
- given). Watching a 5-minute sighting then takes 5 real minutes, not however long the recording itself took to
85
+ given but in the recorder, editing either date clears an explicit `durationSeconds` the pair can replace, so the
86
+ more recent edit is the one that wins rather than being silently outranked). Watching a 5-minute sighting then takes 5 real minutes, not however long the recording itself took to
86
87
  author (e.g. a quick mouse drag) — drag the seek bar directly to skip ahead. The start/end labels around the seek
87
88
  bar show real clock times when `time` has an hour (e.g. `02:45` → `02:50`); otherwise they show `0:00` → the
88
89
  duration actually available (the declared one if known, else the recording's own length). Playback loops by
@@ -232,6 +233,15 @@ scale a case's witness list actually has. If a witness has no `witness.title`, i
232
233
  the URL itself as a last resort. A mismatched `caseId` across the listed witnesses logs a console warning (doesn't
233
234
  block) — likely means unrelated recordings got listed together by mistake.
234
235
 
236
+ **Where two witnesses described different things, the recordings have to show it.** Not that they
237
+ must differ — witnesses often agree, and two matching recordings are then simply true. But a
238
+ difference that exists in the record and not in the files makes the witness picker a control that
239
+ does nothing, and nothing *looks* broken. Chiles-Whitted shipped that way for months: one testimony
240
+ under two names. Its two pilots drew the object differently for Project Sign — the captain a slim
241
+ ribbed cigar with a pointed nose and no windows, the co-pilot a blunt cylinder with two rows of
242
+ lit windows — and only the co-pilot, in the right seat, saw the terminal phase (McDonald's 1968
243
+ cross-check). Each file now carries its own witness's account.
244
+
235
245
  | Member | Kind | Description |
236
246
  |---|---|---|
237
247
  | `src` | attribute | URL of a single `sighting.json` or a witness manifest (above), fetched automatically on connect and whenever the attribute changes |
@@ -290,7 +300,7 @@ interface SightingRecordingJson {
290
300
  endTime?: { year?: number, month?: number, day?: number, hour?: number, minute?: number, second?: number } // alternative to durationSeconds
291
301
  durationSeconds?: number // alternative to endTime; takes precedence if both are set
292
302
  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
293
- place?: { lat: number, lng: number }[]
303
+ place?: { lat: number, lng: number, name?: string }[] // `name` is the fully qualified place name the coordinates were resolved from — see Naming a place
294
304
  witness?: { id?: string, dirName?: string, title?: string, lastName?: string, firstNames?: string[] } // every field optional — supply whichever is known; omit entirely for an anonymous witness
295
305
  caseId?: string // shared by every witness's own sighting.json for the same case — see <rr0-eyewitness>
296
306
  description?: string
@@ -321,6 +331,7 @@ interface SightingRecordingJson {
321
331
  witnessTrack?: { keyframes: Array<{ t: number, pose: { lat?: number, lng?: number, elevationM: number, headingDeg?: number, pitchDeg: number, fovDeg: number } }> }
322
332
  weatherTrack?: { keyframes: Array<{ t: number, weather: Weather }> }
323
333
  weather?: Weather // legacy static fallback for recordings predating weatherTrack
334
+ weatherSource?: { id: string, name: string, url: string } // the meteorological record weatherTrack was looked up from — see Weather is looked up, not remembered. Absent = the witness's own account
324
335
  decor?: DecorObject[] // buildings, trees, streetlights, vehicles, other witnesses — see src/engine/model/Decor.ts
325
336
  }
326
337
  ```
@@ -329,6 +340,132 @@ A shape left out of a later keyframe is **held** at its last recorded state, not
329
340
  keyframe is at `t=5000` is already painted, in that state, from `t=0` (hold-first/hold-last at both ends of a
330
341
  source's own range). To make something stop being visible, keyframe it with `transparency: 1`.
331
342
 
343
+ ### Naming a place
344
+
345
+ Testimony names a place. It says "on the Valensole plateau", "near Socorro", "over Montgomery" —
346
+ never 43.8379 / 5.9840. So the Location group leads with a **Place** field: type a name, press Enter
347
+ (or **Locate**), and the latitude and longitude below are filled from
348
+ [Nominatim](https://nominatim.openstreetmap.org/), OpenStreetMap's own geocoder — which, unlike the
349
+ gazetteer-style services, knows the hamlets, farms and airfields that cases actually happen at.
350
+ A name is often ambiguous, so every candidate stays listed in **Matches** and the best one is
351
+ applied straight away; picking another moves the witness. Results are named in the reader's own
352
+ language, and credited as their licence requires.
353
+
354
+ What gets stored is the *qualified* name the search resolved (`place[].name`), not the two words
355
+ typed — half the interesting cases happen near a village that shares its name with four others, and
356
+ a later reader needs to land on the same spot. A name no geocoder knows is kept as typed, so a
357
+ recording can still say "the lavender field east of the farm" beside coordinates entered by hand.
358
+
359
+ The field reads both ways: move the latitude or longitude by hand and a resolved name is re-derived
360
+ from the new coordinates, or cleared if there is no place there. A name left describing somewhere
361
+ the sighting is no longer at is worse than no name — the recording would state, in writing, that it
362
+ happened there. A name the witness typed themselves is never replaced.
363
+
364
+ **Altitude is above sea level**, and the ground at the location sets its floor: a witness in the
365
+ Alps is not at 0 m, and an editor that offers it invites a recording that says so. The ground's own
366
+ height is read from whichever elevation source is live and shown beside the field. What gets stored
367
+ is unchanged — `ObserverPose.elevationM` stays a height above the local ground, which is what the
368
+ terrain patch is built around.
369
+
370
+ ### Time zones
371
+
372
+ An hour of `utcOffsetHours` is an hour of Earth's rotation *and* a different row of the weather
373
+ record. Pick the witness's own zone (`Europe/Paris`, `America/Denver`, …) and the offset is derived
374
+ from that zone's rules **at the observation's date**: Valensole in July 1965 resolves to UTC+1, not
375
+ the UTC+2 the same place gives today — France only reintroduced summer time in 1976. Change the date
376
+ and it is derived again. The recording stores both: `timeZone` is the rule, `utcOffsetHours` is the
377
+ number it produced, and every consumer keeps reading only the number.
378
+
379
+ The rules come from the platform's own IANA database. What it cannot fix is a zone whose
380
+ *boundaries* are coarse: Montgomery, Alabama is `America/Chicago`, which observed summer time in
381
+ 1948 while Alabama did not. That is why the zone is chosen by the witness rather than derived from
382
+ the coordinates — and why the plain entered offset remains available for exactly those cases.
383
+
384
+ The search runs only when asked, never per keystroke: Nominatim's usage policy allows the first and
385
+ rules out the second.
386
+
387
+ ### Who answered is part of the answer
388
+
389
+ Every kind of real-world data this editor pulls in — places, weather, ground relief, aerial imagery
390
+ — is chosen by a picker sitting **where that data is reported**, with the attribution its licence
391
+ requires next to it:
392
+
393
+ > 2 places found according to `[Nominatim ▾]` © OpenStreetMap
394
+ >
395
+ > From `[ERA5 (Open-Meteo) ▾]` © Copernicus/ECMWF, 2026-08-21 15:30 UTC
396
+
397
+ Those pickers *are* the credits. Naming who the data comes from and letting it be chosen are the
398
+ same act: a static "© OpenStreetMap" tucked beside a field says where today's answer came from but
399
+ hides that it is a choice, and a picker with no attribution credits nobody. Relief and imagery have
400
+ no sentence to sit in, so they get a row under the coordinates whose ground they describe.
401
+
402
+ Most registries hold a single entry today (imagery holds two, Esri and EOX Sentinel-2 cloudless) —
403
+ which is the point: the seam is visible before it is used, and adding an implementation means adding
404
+ one entry to a registry (`placeSources.ts`, `weatherSources.ts`, `terrainSources.ts`), not new
405
+ markup.
406
+
407
+ A stale offset renders midnight over Paris and gives no clue why, so an offset that cannot belong to
408
+ the declared longitude is flagged on the field, with the meridian's own solar time in the tooltip.
409
+
410
+ Deliberately a wide net rather than a precise one. Legal time genuinely departs from solar time,
411
+ sometimes by hours (all of China runs on UTC+8), and the historical rules are worse — the check must
412
+ never cry wolf at a correct "France on UTC+1 in 1965". It flags only what no country has ever done,
413
+ and only as a warning: the recording states the witness's clock, and nothing here knows better than
414
+ the witness.
415
+
416
+ ### Weather is looked up, not remembered
417
+
418
+ The Circumstances group is the one part of this editor that isn't testimony. Weather is a
419
+ measurable fact about a place at an instant, and the recording already states both — so instead of
420
+ leaving a witness (or an author reconstructing a case decades later) to set a cloud-cover slider
421
+ from memory, the editor looks the conditions up from [ERA5](https://open-meteo.com/en/docs/historical-weather-api),
422
+ the ECMWF reanalysis, hourly and worldwide from 1940 on. The fields then show the record's own
423
+ values, **read-only**, above a line naming the dataset and the exact UTC instant they describe (a
424
+ wrong `utcOffsetHours` shows up there before it shows up in the rendered sky). The request that
425
+ produced them is kept in `weatherSource.url`, so the claim stays checkable years later.
426
+
427
+ Two of the fields have no direct counterpart in the record and are *derived* — `cloudDarkness`,
428
+ which is a look rather than a measurement (weighted by which layers hold the cloud, plus rain and
429
+ thunderstorm), and `cloudBaseM`, placed at the lowest deck holding a real share of the sky, from
430
+ Espy's temperature/dew-point spread for a low deck. Both are documented in
431
+ `src/engine/weather/providers/OpenMeteoWeatherProvider.ts`.
432
+
433
+ Unchecking **From weather records** hands the fields back to the witness: the looked-up values stay
434
+ as a starting point, `weatherSource` is dropped, and no later lookup may overwrite them — the same
435
+ "declared outranks deduced" rule [Behind a cloud](#behind-a-cloud) follows. A recording that names
436
+ a `weatherSource` is replayed exactly as authored and never looked up again, so a published case
437
+ file reads identically offline. Nothing is ever locked without a record to show for it: before 1940,
438
+ or with no network, the fields stay editable and the line says which of the two it is.
439
+
440
+ The `weatherTrack` follows the observation rather than flattening it: a keyframe at its start, one
441
+ at every whole hour it runs through, and one at its end. The record is read *between* its hourly
442
+ rows, not snapped to the nearest one — so Wilcox's two hours carry a cloud deck lifting from 800 m
443
+ to 913 m, and even a four-minute sighting gets a track that moves instead of one value repeated.
444
+ (For a short observation the record often genuinely says nothing changed. That is an answer, not a
445
+ missing feature.)
446
+
447
+ The keyframes are placed on the clock playback actually runs on — `timeline.duration` once
448
+ something has been recorded, and the observation's own declared length before that (the same rule
449
+ `Player.durationOverrideMs` implements). That clock *changes length as authoring proceeds*: the
450
+ first recorded shape turns a fifteen-hour span into a few seconds, so the track is re-derived
451
+ whenever it does. Getting either half wrong looks the same from outside — a sighting whose weather
452
+ never changes.
453
+
454
+ It follows the witness too, not just the clock. Half of aviation testimony is given from a cockpit,
455
+ and an aircraft under observation for an hour is a long way from where it started — so each sample
456
+ is looked up at the position the `witnessTrack` puts the witness at that instant. Positions inside
457
+ one ERA5 grid cell (~28 km) are one query, and several cells still travel in a single request, so
458
+ a stationary witness costs exactly what it always did.
459
+
460
+ `scripts/infer-case-weather.ts` runs the same lookup over case files on disk:
461
+
462
+ ```bash
463
+ npx tsx scripts/infer-case-weather.ts --dry-run path/to/sighting.json
464
+ ```
465
+
466
+ It rewrites only `weatherTrack` and `weatherSource`, splicing them into the file's own text so the
467
+ diff shows the weather and nothing else.
468
+
332
469
  ### Behind a cloud
333
470
 
334
471
  `behindCloud` is how a recording says "it disappeared into a cloud" — keyframed like any other
@@ -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",cloudBase:"Base des nuages",elevation:"Altitude",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",according:"selon",sourceElevation:"Relief",sourceImagery:"Imagerie",placeName:"Lieu",placeNamePlaceholder:"Valensole, France",searchPlace:"Localiser",placeMatch:"Correspondances",placeSearching:"Recherche du lieu…",placeMatchFound:"lieu trouvé",placeMatchesFound:"lieux trouvés",placeNotFound:"Aucun lieu de ce nom",placeSearchFailed:"Recherche de lieu momentanément indisponible",utcOffsetImplausible:"Ce fuseau ne correspond pas à la longitude saisie, dont l'heure solaire est UTC{solar} — la scène et le relevé météo obéissent pourtant à ce qui est déclaré ici. Videz le champ pour le déduire de la longitude.",timeZoneManual:"décalage saisi",altitudeAboveSeaLevel:"Altitude du témoin au-dessus du niveau de la mer — le sol du lieu saisi en fixe le minimum : un témoin dans les Alpes n'est pas à 0 m.",groundAt:"sol à {m} m",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:"Date et heure",locationGroup:"Lieu",observationGroup:"Observation",witnessGroup:"Témoin",circumstancesGroup:"Circonstances",weatherInferred:"D'après les relevés",weatherInferredTitle:"La météo n'est pas un témoignage : c'est un fait mesurable, relevé pour la date, l'heure et le lieu de l'observation. Décochez pour saisir les conditions rapportées par le témoin, qui priment alors sur le relevé.",weatherFrom:"D'après",weatherLookingUp:"Recherche du relevé…",weatherNeedsDateAndPlace:"Date complète et lieu requis pour retrouver le relevé",weatherNoRecord:"Aucun relevé pour cette date et ce lieu",weatherLookupFailed:"Relevés météo momentanément inaccessibles",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};