passerelle-geo-components 0.2.0-alpha.1
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 +77 -0
- package/examples/README.md +175 -0
- package/examples/choropleth-demographique.json +90 -0
- package/examples/corpus-documentaire.json +170 -0
- package/examples/diagnostic-temporel.json +160 -0
- package/examples/heatmap-rag.json +84 -0
- package/examples/maquette-3d.json +153 -0
- package/examples/minimal.json +19 -0
- package/examples/multi-layers-narrative.json +119 -0
- package/examples/timeline-overlay-simple.json +90 -0
- package/examples/validation-terrain-zebra.json +98 -0
- package/geo-components.js +1719 -0
- package/package.json +77 -0
- package/schemas/README.md +125 -0
- package/schemas/scene_manifest.pydantic.py +1000 -0
- package/schemas/scene_manifest.schema.json +905 -0
- package/schemas/scene_manifest.zod.ts +66 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: scene_manifest.schema.json
|
|
3
|
+
# timestamp: 2026-07-10T13:35:15+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from pydantic import (
|
|
12
|
+
AnyUrl,
|
|
13
|
+
AwareDatetime,
|
|
14
|
+
BaseModel,
|
|
15
|
+
ConfigDict,
|
|
16
|
+
Field,
|
|
17
|
+
RootModel,
|
|
18
|
+
constr,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Projection(RootModel[str]):
|
|
23
|
+
root: str = Field(
|
|
24
|
+
"mercator",
|
|
25
|
+
description="Projection cartographique. 'mercator' (défaut), 'globe' (MapLibre v5+), 'lambert93' (via adapter proj-lambert93). Enum ouverte : accepte x_<projet>_* avec fallback graceful 'mercator'.",
|
|
26
|
+
pattern="^x_[a-z_]+$",
|
|
27
|
+
title="Extension nommée par projet",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Unit(Enum):
|
|
32
|
+
metric = "metric"
|
|
33
|
+
imperial = "imperial"
|
|
34
|
+
nautical = "nautical"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Producer(RootModel[str]):
|
|
38
|
+
root: str = Field(
|
|
39
|
+
...,
|
|
40
|
+
description="Producteur du manifest. Enum ouverte : accepte x_<projet>_* avec fallback graceful 'manual'.",
|
|
41
|
+
pattern="^x_[a-z_]+$",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RecipeUsed(BaseModel):
|
|
46
|
+
model_config = ConfigDict(
|
|
47
|
+
extra="forbid",
|
|
48
|
+
)
|
|
49
|
+
slug: str | None = None
|
|
50
|
+
version_sha: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Provenance(BaseModel):
|
|
54
|
+
model_config = ConfigDict(
|
|
55
|
+
extra="allow",
|
|
56
|
+
)
|
|
57
|
+
producer: Literal["qgis-sspcloud"] | Literal["cerema-livrables"] | Literal[
|
|
58
|
+
"atlas"
|
|
59
|
+
] | Literal["manual"] | Producer = Field(
|
|
60
|
+
...,
|
|
61
|
+
description="Producteur du manifest. Enum ouverte : accepte x_<projet>_* avec fallback graceful 'manual'.",
|
|
62
|
+
)
|
|
63
|
+
producer_version: str
|
|
64
|
+
source_project: str | None = Field(
|
|
65
|
+
None, description="Chemin projet source (ex: .qgz path)."
|
|
66
|
+
)
|
|
67
|
+
qgz_hash: str | None = Field(None, pattern="^sha256:[0-9a-f]{64}$")
|
|
68
|
+
agent_conversation_id: str | None = Field(
|
|
69
|
+
None, description="ID conversation LLM si issu d'un agent."
|
|
70
|
+
)
|
|
71
|
+
recipe_used: RecipeUsed | None = None
|
|
72
|
+
extensions: dict[
|
|
73
|
+
constr(pattern=r"^[a-z][a-z_0-9]*$"), dict[str, Any]
|
|
74
|
+
] | None = Field(
|
|
75
|
+
None,
|
|
76
|
+
description="Extensions provenance par-projet (ex: atlas.native_payload_hash pour round-trip fidèle).",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Id(RootModel[str]):
|
|
81
|
+
root: str = Field(
|
|
82
|
+
...,
|
|
83
|
+
description="ID du fond de carte. Enum ouverte : accepte x_<projet>_* avec fallback graceful 'osm'.",
|
|
84
|
+
pattern="^x_[a-z_]+$",
|
|
85
|
+
title="Extension nommée par projet",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Toggles(BaseModel):
|
|
90
|
+
model_config = ConfigDict(
|
|
91
|
+
extra="forbid",
|
|
92
|
+
)
|
|
93
|
+
labels: bool = Field(
|
|
94
|
+
True, description="Toggle labels basemap (absorbé de ex-overlays)."
|
|
95
|
+
)
|
|
96
|
+
buildings_3d: bool = Field(
|
|
97
|
+
False, description="Toggle fill-extrusion bâtiments natifs basemap."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Basemap(BaseModel):
|
|
102
|
+
model_config = ConfigDict(
|
|
103
|
+
extra="forbid",
|
|
104
|
+
)
|
|
105
|
+
id: Literal["osm"] | Literal["plan-ign-v2"] | Literal["plan-ign-v2-gris"] | Literal[
|
|
106
|
+
"ortho-ign"
|
|
107
|
+
] | Literal["hillshade-ign"] | Literal["dsfr-sobre"] | Literal["etalab"] | Literal[
|
|
108
|
+
"openfreemap-liberty"
|
|
109
|
+
] | Literal[
|
|
110
|
+
"openfreemap-bright"
|
|
111
|
+
] | Literal[
|
|
112
|
+
"openfreemap-positron"
|
|
113
|
+
] | Literal[
|
|
114
|
+
"cartodb-positron-nolabels"
|
|
115
|
+
] | Literal[
|
|
116
|
+
"custom"
|
|
117
|
+
] | Id = Field(
|
|
118
|
+
...,
|
|
119
|
+
description="ID du fond de carte. Enum ouverte : accepte x_<projet>_* avec fallback graceful 'osm'.",
|
|
120
|
+
)
|
|
121
|
+
style: dict[str, Any] | None = Field(
|
|
122
|
+
None,
|
|
123
|
+
description="Style JSON MapLibre v8 (utilisé si id='custom' ou fallback si id inconnu).",
|
|
124
|
+
)
|
|
125
|
+
alternatives: list[str] | None = Field(
|
|
126
|
+
None, description="IDs alternatifs pour switcher UI runtime."
|
|
127
|
+
)
|
|
128
|
+
toggles: Toggles | None = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Kind(Enum):
|
|
132
|
+
commune = "commune"
|
|
133
|
+
insee_arm = "insee_arm"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Zone2(BaseModel):
|
|
137
|
+
model_config = ConfigDict(
|
|
138
|
+
extra="forbid",
|
|
139
|
+
)
|
|
140
|
+
kind: Kind
|
|
141
|
+
insee: str = Field(
|
|
142
|
+
...,
|
|
143
|
+
description="Code INSEE 5 chars (commune 75001-...; arrondissement 75101-75120, 13201-13216, 69381-69389).",
|
|
144
|
+
pattern="^[0-9AB]{5}$",
|
|
145
|
+
)
|
|
146
|
+
buffer_km: float | None = Field(
|
|
147
|
+
None,
|
|
148
|
+
description="Rayon buffer autour de la commune (facultatif).",
|
|
149
|
+
ge=0.0,
|
|
150
|
+
le=50.0,
|
|
151
|
+
)
|
|
152
|
+
pitch: float | None = Field(None, ge=0.0, le=85.0)
|
|
153
|
+
bearing: float | None = Field(None, ge=-180.0, le=180.0)
|
|
154
|
+
max_pitch: float = Field(60, ge=0.0, le=85.0)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Zone3(BaseModel):
|
|
158
|
+
model_config = ConfigDict(
|
|
159
|
+
extra="forbid",
|
|
160
|
+
)
|
|
161
|
+
kind: Literal["auto_bounds"]
|
|
162
|
+
pitch: float | None = Field(None, ge=0.0, le=85.0)
|
|
163
|
+
bearing: float | None = Field(None, ge=-180.0, le=180.0)
|
|
164
|
+
max_pitch: float = Field(60, ge=0.0, le=85.0)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Zone4(BaseModel):
|
|
168
|
+
model_config = ConfigDict(
|
|
169
|
+
extra="allow",
|
|
170
|
+
)
|
|
171
|
+
kind: str = Field(
|
|
172
|
+
..., description="Extension nommée par projet.", pattern="^x_[a-z_]+$"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class BBox(RootModel[list[float]]):
|
|
177
|
+
root: list[float] = Field(
|
|
178
|
+
...,
|
|
179
|
+
description="Bounding box [west, south, east, north] EPSG:4326 (GeoJSON RFC 7946 §5).",
|
|
180
|
+
max_length=4,
|
|
181
|
+
min_length=4,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Coord(RootModel[list[float]]):
|
|
186
|
+
root: list[float] = Field(
|
|
187
|
+
...,
|
|
188
|
+
description="Coordonnée [longitude, latitude] EPSG:4326.",
|
|
189
|
+
max_length=2,
|
|
190
|
+
min_length=2,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class CameraPreset(BaseModel):
|
|
195
|
+
model_config = ConfigDict(
|
|
196
|
+
extra="forbid",
|
|
197
|
+
)
|
|
198
|
+
id: str = Field(
|
|
199
|
+
..., description="Identifiant unique du preset (top, 3d, street, ...)."
|
|
200
|
+
)
|
|
201
|
+
pitch: float = Field(..., ge=0.0, le=85.0)
|
|
202
|
+
bearing: float = Field(..., ge=-180.0, le=180.0)
|
|
203
|
+
zoom: float = Field(..., ge=0.0, le=22.0)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class SourceId(RootModel[str]):
|
|
207
|
+
root: str = Field(
|
|
208
|
+
...,
|
|
209
|
+
description="Source DEM. Enum ouverte : accepte x_* pour custom.",
|
|
210
|
+
pattern="^x_[a-z_]+$",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Encoding(Enum):
|
|
215
|
+
mapbox = "mapbox"
|
|
216
|
+
terrarium = "terrarium"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Adapter(Enum):
|
|
220
|
+
ignmnt = "ignmnt"
|
|
221
|
+
NoneType_None = None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class BlendMode(Enum):
|
|
225
|
+
multiply = "multiply"
|
|
226
|
+
normal = "normal"
|
|
227
|
+
overlay = "overlay"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class Hillshade(BaseModel):
|
|
231
|
+
model_config = ConfigDict(
|
|
232
|
+
extra="forbid",
|
|
233
|
+
)
|
|
234
|
+
opacity: float = Field(0.35, ge=0.0, le=1.0)
|
|
235
|
+
blend_mode: BlendMode = "multiply"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class Terrain(BaseModel):
|
|
239
|
+
model_config = ConfigDict(
|
|
240
|
+
extra="forbid",
|
|
241
|
+
)
|
|
242
|
+
source_id: Literal["ign-lidar"] | Literal["terrarium"] | Literal[
|
|
243
|
+
"custom"
|
|
244
|
+
] | SourceId = Field(
|
|
245
|
+
..., description="Source DEM. Enum ouverte : accepte x_* pour custom."
|
|
246
|
+
)
|
|
247
|
+
encoding: Encoding = "mapbox"
|
|
248
|
+
exaggeration: float = Field(1.0, ge=0.0, le=10.0)
|
|
249
|
+
adapter: Adapter | None = Field(
|
|
250
|
+
None,
|
|
251
|
+
description="Adapter chargé à la demande (registre ADAPTERS fermé). null si source_id ne nécessite pas d'adapter.",
|
|
252
|
+
)
|
|
253
|
+
custom_tiles_url: AnyUrl | None = Field(
|
|
254
|
+
None, description="URL tiles raster-dem si source_id='custom'."
|
|
255
|
+
)
|
|
256
|
+
hillshade: Hillshade | None = Field(
|
|
257
|
+
None, description="Effet hillshade (absorbe l'ex overlays.hillshade)."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class LocationForSuncalc(BaseModel):
|
|
262
|
+
model_config = ConfigDict(
|
|
263
|
+
extra="forbid",
|
|
264
|
+
)
|
|
265
|
+
lat: float | None = Field(None, ge=-90.0, le=90.0)
|
|
266
|
+
lng: float | None = Field(None, ge=-180.0, le=180.0)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class Sky(BaseModel):
|
|
270
|
+
model_config = ConfigDict(
|
|
271
|
+
extra="forbid",
|
|
272
|
+
)
|
|
273
|
+
enabled: bool = True
|
|
274
|
+
dynamic_sun: bool = Field(
|
|
275
|
+
True,
|
|
276
|
+
description="Recalcul SunCalc auto selon sun_datetime + location_for_suncalc.",
|
|
277
|
+
)
|
|
278
|
+
sun_datetime: AwareDatetime | None = None
|
|
279
|
+
location_for_suncalc: LocationForSuncalc | None = None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class Role(RootModel[str]):
|
|
283
|
+
root: str = Field(
|
|
284
|
+
...,
|
|
285
|
+
description="Rôle sémantique. Enum ouverte + fallback 'primary'.",
|
|
286
|
+
pattern="^x_[a-z_]+$",
|
|
287
|
+
title="Rôle métier custom (fallback: primary).",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class GeometryType(Enum):
|
|
292
|
+
point = "point"
|
|
293
|
+
line = "line"
|
|
294
|
+
polygon = "polygon"
|
|
295
|
+
multipolygon = "multipolygon"
|
|
296
|
+
raster = "raster"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class AttributeStats(BaseModel):
|
|
300
|
+
model_config = ConfigDict(
|
|
301
|
+
extra="allow",
|
|
302
|
+
)
|
|
303
|
+
min: float | None = None
|
|
304
|
+
max: float | None = None
|
|
305
|
+
null_count: int | None = None
|
|
306
|
+
distinct_count: int | None = None
|
|
307
|
+
distinct_values: list[Any] | None = None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class Data(BaseModel):
|
|
311
|
+
model_config = ConfigDict(
|
|
312
|
+
extra="allow",
|
|
313
|
+
)
|
|
314
|
+
type: Literal["FeatureCollection"]
|
|
315
|
+
features: list[Any]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class LayerSource1(BaseModel):
|
|
319
|
+
model_config = ConfigDict(
|
|
320
|
+
extra="allow",
|
|
321
|
+
)
|
|
322
|
+
type: Literal["geojson"]
|
|
323
|
+
data: AnyUrl | Data = Field(
|
|
324
|
+
...,
|
|
325
|
+
description="FeatureCollection inline OU URL. AUTORISÉ UNIQUEMENT si n_features ≤ 500 (500 KB) — sinon warning validation, interdit au-delà de 5 MB.",
|
|
326
|
+
)
|
|
327
|
+
crs: Literal["EPSG:4326"] = "EPSG:4326"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class LayerSource2(BaseModel):
|
|
331
|
+
model_config = ConfigDict(
|
|
332
|
+
extra="allow",
|
|
333
|
+
)
|
|
334
|
+
type: Literal["geojson_path"]
|
|
335
|
+
path: str = Field(..., description="Chemin PVC ou URL relative (résolu côté hub).")
|
|
336
|
+
crs: Literal["EPSG:4326"] = "EPSG:4326"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class LayerSource3(BaseModel):
|
|
340
|
+
model_config = ConfigDict(
|
|
341
|
+
extra="allow",
|
|
342
|
+
)
|
|
343
|
+
type: Literal["vector"]
|
|
344
|
+
url: str = Field(
|
|
345
|
+
..., description="URL template MapLibre : https://.../{z}/{x}/{y}.pbf"
|
|
346
|
+
)
|
|
347
|
+
source_layer: str
|
|
348
|
+
min_zoom: int | None = Field(None, ge=0, le=22)
|
|
349
|
+
max_zoom: int | None = Field(None, ge=0, le=22)
|
|
350
|
+
promote_id: str | None = None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class LayerSource4(BaseModel):
|
|
354
|
+
model_config = ConfigDict(
|
|
355
|
+
extra="allow",
|
|
356
|
+
)
|
|
357
|
+
type: Literal["pmtiles"]
|
|
358
|
+
url: AnyUrl = Field(
|
|
359
|
+
...,
|
|
360
|
+
description="URL fichier .pmtiles (résolu via pmtiles:// protocol MapLibre).",
|
|
361
|
+
)
|
|
362
|
+
source_layer: str
|
|
363
|
+
promote_id: str | None = None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class Sync(Enum):
|
|
367
|
+
load = "load"
|
|
368
|
+
reactive = "reactive"
|
|
369
|
+
save = "save"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class LayerSource5(BaseModel):
|
|
373
|
+
model_config = ConfigDict(
|
|
374
|
+
extra="allow",
|
|
375
|
+
)
|
|
376
|
+
type: Literal["grist_table"]
|
|
377
|
+
table: str = Field(..., description="Nom table Grist dans le doc courant.")
|
|
378
|
+
id_field: str = "id"
|
|
379
|
+
sync: Sync = Field(
|
|
380
|
+
"load",
|
|
381
|
+
description="load: one-shot fetch. reactive: subscribe onRecords. save: bidirectionnel.",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class LayerSource6(BaseModel):
|
|
386
|
+
model_config = ConfigDict(
|
|
387
|
+
extra="allow",
|
|
388
|
+
)
|
|
389
|
+
type: Literal["wfs"]
|
|
390
|
+
url: AnyUrl
|
|
391
|
+
type_name: str = Field(..., description="WFS TypeName (ex: BATIMENT.BATIMENT).")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class LayerSource7(BaseModel):
|
|
395
|
+
model_config = ConfigDict(
|
|
396
|
+
extra="allow",
|
|
397
|
+
)
|
|
398
|
+
type: Literal["boundary_admin"]
|
|
399
|
+
catalog_id: str = Field(
|
|
400
|
+
...,
|
|
401
|
+
description="ID catalog IGN admin (arrondissements_marseille, communes, epci, ...).",
|
|
402
|
+
)
|
|
403
|
+
filter: dict[str, Any] | None = Field(
|
|
404
|
+
None, description="Filtre attributaire (ex: {insee_arm: '13204'})."
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class LayerSource8(BaseModel):
|
|
409
|
+
model_config = ConfigDict(
|
|
410
|
+
extra="allow",
|
|
411
|
+
)
|
|
412
|
+
type: str = Field(
|
|
413
|
+
..., description="Extension source nommée par projet.", pattern="^x_[a-z_]+$"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class LayerSource(
|
|
418
|
+
RootModel[
|
|
419
|
+
LayerSource1
|
|
420
|
+
| LayerSource2
|
|
421
|
+
| LayerSource3
|
|
422
|
+
| LayerSource4
|
|
423
|
+
| LayerSource5
|
|
424
|
+
| LayerSource6
|
|
425
|
+
| LayerSource7
|
|
426
|
+
| LayerSource8
|
|
427
|
+
]
|
|
428
|
+
):
|
|
429
|
+
root: LayerSource1 | LayerSource2 | LayerSource3 | LayerSource4 | LayerSource5 | LayerSource6 | LayerSource7 | LayerSource8 = Field(
|
|
430
|
+
...,
|
|
431
|
+
description="Source de données. Discriminated union sur 'type'. Alignée MapLibre style spec (v8+).",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class Extrusion(BaseModel):
|
|
436
|
+
model_config = ConfigDict(
|
|
437
|
+
extra="forbid",
|
|
438
|
+
)
|
|
439
|
+
enabled: bool = Field(
|
|
440
|
+
False, description="Active fill-extrusion 2.5D (dérive dimension='2.5d')."
|
|
441
|
+
)
|
|
442
|
+
height_field: str | None = None
|
|
443
|
+
base_field: str | None = None
|
|
444
|
+
vertical_gradient: bool = True
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class Outline(BaseModel):
|
|
448
|
+
model_config = ConfigDict(
|
|
449
|
+
extra="forbid",
|
|
450
|
+
)
|
|
451
|
+
enabled: bool = False
|
|
452
|
+
color: str = Field("#000091", pattern="^#[0-9a-fA-F]{6}$")
|
|
453
|
+
width: float = Field(1, ge=0.0)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class HollowPoint(BaseModel):
|
|
457
|
+
model_config = ConfigDict(
|
|
458
|
+
extra="forbid",
|
|
459
|
+
)
|
|
460
|
+
enabled: bool = False
|
|
461
|
+
stroke_color_from_field: str | None = None
|
|
462
|
+
radius: float = Field(7, ge=0.0)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class Opacity1(BaseModel):
|
|
466
|
+
model_config = ConfigDict(
|
|
467
|
+
extra="forbid",
|
|
468
|
+
)
|
|
469
|
+
kind: Literal["static"]
|
|
470
|
+
value: float = Field(..., ge=0.0, le=1.0)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class Opacity2(BaseModel):
|
|
474
|
+
model_config = ConfigDict(
|
|
475
|
+
extra="forbid",
|
|
476
|
+
)
|
|
477
|
+
kind: Literal["reactive"]
|
|
478
|
+
prop: str = Field(
|
|
479
|
+
..., description="Event geo:bind prop (time, scenario, filter, ...)."
|
|
480
|
+
)
|
|
481
|
+
matched: float = Field(..., ge=0.0, le=1.0)
|
|
482
|
+
before: float = Field(..., ge=0.0, le=1.0)
|
|
483
|
+
after: float = Field(..., ge=0.0, le=1.0)
|
|
484
|
+
null_value: float | None = Field(None, ge=0.0, le=1.0)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class Opacity(RootModel[Opacity1 | Opacity2]):
|
|
488
|
+
root: Opacity1 | Opacity2 = Field(
|
|
489
|
+
..., description="Opacité statique ou réactive (bindée sur événement geo:bind)."
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class Label(BaseModel):
|
|
494
|
+
model_config = ConfigDict(
|
|
495
|
+
extra="forbid",
|
|
496
|
+
)
|
|
497
|
+
enabled: bool = False
|
|
498
|
+
field: str | None = None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class ColorClassification1(BaseModel):
|
|
502
|
+
model_config = ConfigDict(
|
|
503
|
+
extra="forbid",
|
|
504
|
+
)
|
|
505
|
+
mode: Literal["single"]
|
|
506
|
+
value: str = Field(..., pattern="^#[0-9a-fA-F]{6}$")
|
|
507
|
+
default: str | None = Field(None, pattern="^#[0-9a-fA-F]{6}$")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class Category(BaseModel):
|
|
511
|
+
model_config = ConfigDict(
|
|
512
|
+
extra="forbid",
|
|
513
|
+
)
|
|
514
|
+
value: str | float | bool | None
|
|
515
|
+
color: str = Field(..., pattern="^#[0-9a-fA-F]{6}$")
|
|
516
|
+
count: int | None = Field(None, ge=0)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class ColorClassification2(BaseModel):
|
|
520
|
+
model_config = ConfigDict(
|
|
521
|
+
extra="forbid",
|
|
522
|
+
)
|
|
523
|
+
mode: Literal["categorized"]
|
|
524
|
+
field: str
|
|
525
|
+
categories: list[Category]
|
|
526
|
+
default: str = Field(..., pattern="^#[0-9a-fA-F]{6}$")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class Method(Enum):
|
|
530
|
+
linear = "linear"
|
|
531
|
+
log = "log"
|
|
532
|
+
sqrt = "sqrt"
|
|
533
|
+
jenks = "jenks"
|
|
534
|
+
quantile = "quantile"
|
|
535
|
+
equal_interval = "equal_interval"
|
|
536
|
+
manual = "manual"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class ColorClassification3(BaseModel):
|
|
540
|
+
model_config = ConfigDict(
|
|
541
|
+
extra="allow",
|
|
542
|
+
)
|
|
543
|
+
mode: Literal["graduated"]
|
|
544
|
+
field: str
|
|
545
|
+
method: Method = Field(
|
|
546
|
+
...,
|
|
547
|
+
description="Méthode. INPUT agent : single/categorized/manual/linear. COMPUTED producteur : + jenks/quantile/equal_interval/log/sqrt (pré-calculés en 'manual').",
|
|
548
|
+
)
|
|
549
|
+
breaks: list[float] = Field(
|
|
550
|
+
..., description="Bornes intérieures. N breaks = N+1 classes.", min_length=1
|
|
551
|
+
)
|
|
552
|
+
palette: str = Field(..., description="Nom palette (voir §4.3 CONTRACT-V0.3.1.md).")
|
|
553
|
+
default: str = Field(..., pattern="^#[0-9a-fA-F]{6}$")
|
|
554
|
+
field_compiled_expression: list[Any] | None = Field(
|
|
555
|
+
None,
|
|
556
|
+
alias="_compiled_expression",
|
|
557
|
+
description="MapLibre expression compilée. Source de vérité rendu. readOnly (généré producteur).",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class ColorClassification4(BaseModel):
|
|
562
|
+
model_config = ConfigDict(
|
|
563
|
+
extra="forbid",
|
|
564
|
+
)
|
|
565
|
+
mode: Literal["expression"]
|
|
566
|
+
expr: list[Any] = Field(
|
|
567
|
+
...,
|
|
568
|
+
description="MapLibre expression brute passthrough (non-portable renderers non-MapLibre).",
|
|
569
|
+
)
|
|
570
|
+
default: str | None = Field(None, pattern="^#[0-9a-fA-F]{6}$")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
class ColorClassification(
|
|
574
|
+
RootModel[
|
|
575
|
+
ColorClassification1
|
|
576
|
+
| ColorClassification2
|
|
577
|
+
| ColorClassification3
|
|
578
|
+
| ColorClassification4
|
|
579
|
+
]
|
|
580
|
+
):
|
|
581
|
+
root: ColorClassification1 | ColorClassification2 | ColorClassification3 | ColorClassification4 = Field(
|
|
582
|
+
...,
|
|
583
|
+
description="Discriminated union sur 'mode'. Voir §4.4 CONTRACT-V0.3.1.md pour combinaisons valides.",
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class SizeClassification1(BaseModel):
|
|
588
|
+
model_config = ConfigDict(
|
|
589
|
+
extra="forbid",
|
|
590
|
+
)
|
|
591
|
+
mode: Literal["single"]
|
|
592
|
+
value: float = Field(..., ge=0.0)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class OutputRangeItem(RootModel[float]):
|
|
596
|
+
root: float = Field(..., ge=0.0)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class Method1(Enum):
|
|
600
|
+
linear = "linear"
|
|
601
|
+
log = "log"
|
|
602
|
+
sqrt = "sqrt"
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class SizeClassification2(BaseModel):
|
|
606
|
+
model_config = ConfigDict(
|
|
607
|
+
extra="forbid",
|
|
608
|
+
)
|
|
609
|
+
mode: Literal["graduated"]
|
|
610
|
+
field: str
|
|
611
|
+
output_range: list[OutputRangeItem] = Field(..., max_length=2, min_length=2)
|
|
612
|
+
method: Method1 = "linear"
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class SizeClassification(RootModel[SizeClassification1 | SizeClassification2]):
|
|
616
|
+
root: SizeClassification1 | SizeClassification2 = Field(
|
|
617
|
+
...,
|
|
618
|
+
description="Discriminated union sur 'mode' pour size (cercles proportionnels, extrusion, ...).",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class ModelClassification1(BaseModel):
|
|
623
|
+
model_config = ConfigDict(
|
|
624
|
+
extra="forbid",
|
|
625
|
+
)
|
|
626
|
+
mode: Literal["single"]
|
|
627
|
+
value: str | None = Field(None, description="modelId ou null.")
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class Category1(BaseModel):
|
|
631
|
+
model_config = ConfigDict(
|
|
632
|
+
extra="forbid",
|
|
633
|
+
)
|
|
634
|
+
value: str | float | bool
|
|
635
|
+
model_id: str
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class ModelClassification2(BaseModel):
|
|
639
|
+
model_config = ConfigDict(
|
|
640
|
+
extra="forbid",
|
|
641
|
+
)
|
|
642
|
+
mode: Literal["categorized"]
|
|
643
|
+
field: str
|
|
644
|
+
categories: list[Category1]
|
|
645
|
+
default_model_id: str | None
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class ModelClassification(RootModel[ModelClassification1 | ModelClassification2]):
|
|
649
|
+
root: ModelClassification1 | ModelClassification2 = Field(
|
|
650
|
+
..., description="Discriminated union pour choix modèle 3D (Atlas)."
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class Kind1(Enum):
|
|
655
|
+
handlebars = "handlebars"
|
|
656
|
+
html = "html"
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class PopupTemplate(BaseModel):
|
|
660
|
+
model_config = ConfigDict(
|
|
661
|
+
extra="forbid",
|
|
662
|
+
)
|
|
663
|
+
kind: Kind1
|
|
664
|
+
body: str = Field(..., max_length=4000)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class Interactions(BaseModel):
|
|
668
|
+
model_config = ConfigDict(
|
|
669
|
+
extra="forbid",
|
|
670
|
+
)
|
|
671
|
+
popup_template: PopupTemplate | None = None
|
|
672
|
+
tooltip_field: str | None = None
|
|
673
|
+
hover_attributes: list[str] | None = None
|
|
674
|
+
emits_events: bool = Field(
|
|
675
|
+
True,
|
|
676
|
+
description="Si true, émet geo:feature-click/hover canoniques. Noms d'événements fixés par la lib (voir §4.5).",
|
|
677
|
+
)
|
|
678
|
+
feature_state_selected_field: str | None = Field(
|
|
679
|
+
None,
|
|
680
|
+
description="Attribut identifiant pour setFeatureState('selected') MapLibre (bidirectionnel Grist bridge).",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class Kind2(RootModel[str]):
|
|
685
|
+
root: str = Field(
|
|
686
|
+
...,
|
|
687
|
+
description="Type sémantique du contenu (renderer neutre). Enum ouverte.",
|
|
688
|
+
pattern="^x_[a-z_]+$",
|
|
689
|
+
title="Extension custom layer nommée par projet.",
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class Adapter1(RootModel[str]):
|
|
694
|
+
root: str = Field(
|
|
695
|
+
...,
|
|
696
|
+
description="Adapter registre ADAPTERS de la lib (chargé à la demande).",
|
|
697
|
+
pattern="^x_[a-z_]+$",
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class CustomLayer(BaseModel):
|
|
702
|
+
model_config = ConfigDict(
|
|
703
|
+
extra="forbid",
|
|
704
|
+
)
|
|
705
|
+
id: str
|
|
706
|
+
kind: Literal["gltf_instances"] | Literal["custom_3d_layer"] | Kind2 = Field(
|
|
707
|
+
..., description="Type sémantique du contenu (renderer neutre). Enum ouverte."
|
|
708
|
+
)
|
|
709
|
+
adapter: Literal["geo-3d-scene"] | Adapter1 = Field(
|
|
710
|
+
..., description="Adapter registre ADAPTERS de la lib (chargé à la demande)."
|
|
711
|
+
)
|
|
712
|
+
source_layer_id: str | None = Field(
|
|
713
|
+
None,
|
|
714
|
+
description="Référence un layers[].id porteur des features instances (P0-K, évite instances[] inline).",
|
|
715
|
+
)
|
|
716
|
+
params: dict[str, Any] | None = Field(
|
|
717
|
+
None, description="Params spécifiques adapter (gltf_base_url, shadow_map, ...)."
|
|
718
|
+
)
|
|
719
|
+
extensions: dict[constr(pattern=r"^[a-z][a-z_0-9]*$"), dict[str, Any]] | None = None
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class Item(BaseModel):
|
|
723
|
+
model_config = ConfigDict(
|
|
724
|
+
extra="forbid",
|
|
725
|
+
)
|
|
726
|
+
label: str
|
|
727
|
+
color: str | None = Field(None, pattern="^#[0-9a-fA-F]{6}$")
|
|
728
|
+
count: int | None = Field(None, ge=0)
|
|
729
|
+
size: float | None = Field(None, ge=0.0)
|
|
730
|
+
layer: str | None = None
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class LegendFormat(Enum):
|
|
734
|
+
chips = "chips"
|
|
735
|
+
gradient_bar = "gradient_bar"
|
|
736
|
+
proportional = "proportional"
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class OverlayPosition(Enum):
|
|
740
|
+
top_left = "top-left"
|
|
741
|
+
top_right = "top-right"
|
|
742
|
+
bottom_left = "bottom-left"
|
|
743
|
+
bottom_right = "bottom-right"
|
|
744
|
+
bottom = "bottom"
|
|
745
|
+
right = "right"
|
|
746
|
+
floating = "floating"
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class Bind(RootModel[str]):
|
|
750
|
+
root: str = Field(
|
|
751
|
+
...,
|
|
752
|
+
description="Source de mise à jour du compteur. Enum ouverte.",
|
|
753
|
+
pattern="^x_[a-z_]+$",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class Expr(RootModel[str]):
|
|
758
|
+
root: str = Field(..., description="Expression du compteur.", pattern="^x_[a-z_]+$")
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class Counter(BaseModel):
|
|
762
|
+
model_config = ConfigDict(
|
|
763
|
+
extra="forbid",
|
|
764
|
+
)
|
|
765
|
+
id: str
|
|
766
|
+
bind: Literal["time"] | Literal["scenario"] | Literal["filter"] | Literal[
|
|
767
|
+
"selection"
|
|
768
|
+
] | Bind = Field(
|
|
769
|
+
..., description="Source de mise à jour du compteur. Enum ouverte."
|
|
770
|
+
)
|
|
771
|
+
expr: Literal["filtered_count"] | Literal["total"] | Literal[
|
|
772
|
+
"percentage"
|
|
773
|
+
] | Expr = Field(..., description="Expression du compteur.")
|
|
774
|
+
template: str = Field(
|
|
775
|
+
..., description="Template Handlebars avec vars {{count}}, {{total}}, {{pct}}."
|
|
776
|
+
)
|
|
777
|
+
position: OverlayPosition | None = "bottom-left"
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
class Scalebar(BaseModel):
|
|
781
|
+
model_config = ConfigDict(
|
|
782
|
+
extra="forbid",
|
|
783
|
+
)
|
|
784
|
+
position: OverlayPosition | None = "bottom-left"
|
|
785
|
+
unit: Unit = "metric"
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
class NorthArrow(BaseModel):
|
|
789
|
+
model_config = ConfigDict(
|
|
790
|
+
extra="forbid",
|
|
791
|
+
)
|
|
792
|
+
position: OverlayPosition | None = "bottom-left"
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
class Zone1(BaseModel):
|
|
796
|
+
model_config = ConfigDict(
|
|
797
|
+
extra="forbid",
|
|
798
|
+
)
|
|
799
|
+
kind: Literal["manual"]
|
|
800
|
+
bbox: BBox | None = None
|
|
801
|
+
center: Coord | None = None
|
|
802
|
+
zoom: float | None = Field(None, ge=0.0, le=22.0)
|
|
803
|
+
pitch: float | None = Field(None, ge=0.0, le=85.0)
|
|
804
|
+
bearing: float | None = Field(None, ge=-180.0, le=180.0)
|
|
805
|
+
max_pitch: float = Field(60, ge=0.0, le=85.0)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
class Zone(RootModel[Zone1 | Zone2 | Zone3 | Zone4]):
|
|
809
|
+
root: Zone1 | Zone2 | Zone3 | Zone4 = Field(
|
|
810
|
+
...,
|
|
811
|
+
description="Zone d'étude / vue caméra initiale. Discriminated union sur 'kind'.",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class Classification(BaseModel):
|
|
816
|
+
model_config = ConfigDict(
|
|
817
|
+
extra="allow",
|
|
818
|
+
)
|
|
819
|
+
color: ColorClassification | None = None
|
|
820
|
+
size: SizeClassification | None = None
|
|
821
|
+
model: ModelClassification | None = None
|
|
822
|
+
label: Label | None = None
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class Legend1(BaseModel):
|
|
826
|
+
model_config = ConfigDict(
|
|
827
|
+
extra="forbid",
|
|
828
|
+
)
|
|
829
|
+
mode: Literal["auto"]
|
|
830
|
+
from_layer: str = Field(..., description="layers[].id source de la classification.")
|
|
831
|
+
position: OverlayPosition | None = "bottom-left"
|
|
832
|
+
format: LegendFormat | None = "chips"
|
|
833
|
+
title: str | None = Field(None, max_length=200)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class Legend2(BaseModel):
|
|
837
|
+
model_config = ConfigDict(
|
|
838
|
+
extra="forbid",
|
|
839
|
+
)
|
|
840
|
+
mode: Literal["manual"]
|
|
841
|
+
items: list[Item]
|
|
842
|
+
position: OverlayPosition | None = "bottom-left"
|
|
843
|
+
format: LegendFormat | None = "chips"
|
|
844
|
+
title: str | None = Field(None, max_length=200)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class Legend(RootModel[Legend1 | Legend2]):
|
|
848
|
+
root: Legend1 | Legend2 = Field(
|
|
849
|
+
..., description="Légende. Discriminated union sur 'mode'."
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
class LayerStyle(BaseModel):
|
|
854
|
+
model_config = ConfigDict(
|
|
855
|
+
extra="allow",
|
|
856
|
+
)
|
|
857
|
+
visible: bool = True
|
|
858
|
+
z_index: int | None = Field(
|
|
859
|
+
None,
|
|
860
|
+
description="Ordre d'affichage (plus élevé = au-dessus). null = ordre du tableau.",
|
|
861
|
+
)
|
|
862
|
+
opacity: Opacity | None = None
|
|
863
|
+
classification: Classification | None = None
|
|
864
|
+
extrusion: Extrusion | None = None
|
|
865
|
+
outline: Outline | None = Field(
|
|
866
|
+
None,
|
|
867
|
+
description="Ajoute automatiquement un layer 'line' sur la même source (absorbe workaround Livrables).",
|
|
868
|
+
)
|
|
869
|
+
hollow_point: HollowPoint | None = Field(
|
|
870
|
+
None,
|
|
871
|
+
description="Point creux pour features approximatives (absorbe workaround Livrables addPoints).",
|
|
872
|
+
)
|
|
873
|
+
maplibre_paint_override: dict[str, Any] | None = Field(
|
|
874
|
+
None,
|
|
875
|
+
description="Passthrough MapLibre setPaintProperty pour props avancées non-couvertes par classification.",
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
class Layer(BaseModel):
|
|
880
|
+
model_config = ConfigDict(
|
|
881
|
+
extra="forbid",
|
|
882
|
+
)
|
|
883
|
+
id: str = Field(
|
|
884
|
+
...,
|
|
885
|
+
description="Identifiant slug unique dans le manifest.",
|
|
886
|
+
pattern="^[a-z][a-z0-9_-]*$",
|
|
887
|
+
)
|
|
888
|
+
name: str = Field(..., description="Nom affiché en légende.", max_length=300)
|
|
889
|
+
role: Literal["primary"] | Literal["context"] | Literal["emprise"] | Literal[
|
|
890
|
+
"members"
|
|
891
|
+
] | Literal["site"] | Role = Field(
|
|
892
|
+
..., description="Rôle sémantique. Enum ouverte + fallback 'primary'."
|
|
893
|
+
)
|
|
894
|
+
geometry_type: GeometryType = Field(
|
|
895
|
+
...,
|
|
896
|
+
description="Type géométrie MapLibre (guide l'auto-split polygon+point si Feature Collection mixte).",
|
|
897
|
+
)
|
|
898
|
+
source: LayerSource
|
|
899
|
+
n_features: int | None = Field(
|
|
900
|
+
None,
|
|
901
|
+
description="Nombre de features (pré-calculé — obligatoire si source.type ≠ geojson).",
|
|
902
|
+
ge=0,
|
|
903
|
+
)
|
|
904
|
+
bbox: BBox | None = None
|
|
905
|
+
attribute_stats: dict[
|
|
906
|
+
constr(pattern=r"^[a-zA-Z_][a-zA-Z0-9_]*$"), AttributeStats
|
|
907
|
+
] | None = None
|
|
908
|
+
style: LayerStyle
|
|
909
|
+
interactions: Interactions | None = None
|
|
910
|
+
min_zoom: float | None = Field(None, ge=0.0, le=22.0)
|
|
911
|
+
max_zoom: float | None = Field(None, ge=0.0, le=22.0)
|
|
912
|
+
extensions: dict[constr(pattern=r"^[a-z][a-z_0-9]*$"), dict[str, Any]] | None = None
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
class ScenemanifestV031(BaseModel):
|
|
916
|
+
model_config = ConfigDict(
|
|
917
|
+
extra="forbid",
|
|
918
|
+
)
|
|
919
|
+
field_schema: AnyUrl | None = Field(
|
|
920
|
+
None,
|
|
921
|
+
alias="$schema",
|
|
922
|
+
description="Pointeur JSON Schema self-description (facultatif si absent).",
|
|
923
|
+
)
|
|
924
|
+
manifest_version: str = Field(
|
|
925
|
+
...,
|
|
926
|
+
description="Version SemVer strict du contract. V0.3.x lit tout 0.3.y avec y ≤ x (compat descendante 2 versions mineures).",
|
|
927
|
+
pattern="^0\\.3\\.[0-9]+$",
|
|
928
|
+
)
|
|
929
|
+
scene_hash: str | None = Field(
|
|
930
|
+
None,
|
|
931
|
+
description="Hash SHA-256 canonique du manifest_body. Calculé UNIQUEMENT côté producteur (au snapshot dans scene_store/). Le consommateur ne recalcule jamais côté main thread. Helper computeSceneHashInWorker() disponible pour vérification offline.",
|
|
932
|
+
pattern="^sha256:[0-9a-f]{64}$",
|
|
933
|
+
)
|
|
934
|
+
manifest_id: UUID | None = Field(
|
|
935
|
+
None, description="UUID4 unique du manifest. Généré au publish."
|
|
936
|
+
)
|
|
937
|
+
produced_at: AwareDatetime = Field(
|
|
938
|
+
..., description="Timestamp ISO 8601 avec TZ. Généré au publish."
|
|
939
|
+
)
|
|
940
|
+
title: str = Field(
|
|
941
|
+
...,
|
|
942
|
+
description="Titre principal de la scène (affiché en header carto).",
|
|
943
|
+
max_length=300,
|
|
944
|
+
)
|
|
945
|
+
subtitle: str | None = Field(
|
|
946
|
+
None, description="Sous-titre optionnel.", max_length=300
|
|
947
|
+
)
|
|
948
|
+
description: str | None = Field(
|
|
949
|
+
None, description="Description longue (paragraphe intro).", max_length=2000
|
|
950
|
+
)
|
|
951
|
+
source_text: str | None = Field(
|
|
952
|
+
None,
|
|
953
|
+
description="Citation datée des sources de données (BD TOPO IGN 2024, ...).",
|
|
954
|
+
max_length=500,
|
|
955
|
+
)
|
|
956
|
+
caveat: str | None = Field(
|
|
957
|
+
None,
|
|
958
|
+
description="Disclaimer méthodologique (marge d'erreur, données pédagogiques, ...).",
|
|
959
|
+
max_length=2000,
|
|
960
|
+
)
|
|
961
|
+
provenance: Provenance
|
|
962
|
+
projection: Literal["mercator"] | Literal["globe"] | Literal[
|
|
963
|
+
"lambert93"
|
|
964
|
+
] | Projection = Field(
|
|
965
|
+
"mercator",
|
|
966
|
+
description="Projection cartographique. 'mercator' (défaut), 'globe' (MapLibre v5+), 'lambert93' (via adapter proj-lambert93). Enum ouverte : accepte x_<projet>_* avec fallback graceful 'mercator'.",
|
|
967
|
+
validate_default=True,
|
|
968
|
+
)
|
|
969
|
+
basemap: Basemap
|
|
970
|
+
basemap_switcher: list[str] | None = Field(
|
|
971
|
+
None,
|
|
972
|
+
description="IDs de basemaps proposés en toggle runtime UI (opt.). Défaut : uniquement basemap.id actuel.",
|
|
973
|
+
)
|
|
974
|
+
zone: Zone
|
|
975
|
+
camera_presets: list[CameraPreset] | None = Field(
|
|
976
|
+
None,
|
|
977
|
+
description="Presets caméra nommés (top, 3d, street, ...). Utilisés par API impérative flyToPreset().",
|
|
978
|
+
)
|
|
979
|
+
terrain: Terrain | None = None
|
|
980
|
+
sky: Sky | None = None
|
|
981
|
+
layers: list[Layer] = Field(
|
|
982
|
+
...,
|
|
983
|
+
description="Couches cartographiques. Ordre = ordre de rendu (z_index respecté si défini).",
|
|
984
|
+
)
|
|
985
|
+
custom_layers: list[CustomLayer] | None = Field(
|
|
986
|
+
None,
|
|
987
|
+
description="Couches custom Three.js. Trigger dimension='3d' auto (lib dérivée). Chargé uniquement si <geo-3d-scene> composition slot présent.",
|
|
988
|
+
)
|
|
989
|
+
legend: Legend | None = None
|
|
990
|
+
scalebar: Scalebar | None = None
|
|
991
|
+
north_arrow: NorthArrow | None = None
|
|
992
|
+
counters: list[Counter] | None = Field(
|
|
993
|
+
None, description="Compteurs live bindés (nb features filtrés, %, ...)."
|
|
994
|
+
)
|
|
995
|
+
extensions: dict[
|
|
996
|
+
constr(pattern=r"^[a-z][a-z_0-9]*$"), dict[str, Any]
|
|
997
|
+
] | None = Field(
|
|
998
|
+
None,
|
|
999
|
+
description="Extensions par-projet indexees par prefix (x_livrables, x_atlas, x_sig, x_zebra, ...). Contenu libre (additionalProperties: true).",
|
|
1000
|
+
)
|