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