passerelle-geo-components 0.2.0-alpha.3 → 0.2.0-alpha.5

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/geo-components.js CHANGED
@@ -231,6 +231,9 @@ function _v031ToLegacyShim(params, hostContext) {
231
231
  const catalog = [];
232
232
  const overrides = [];
233
233
  for (const l of params.layers) {
234
+ // V0.3.2 alpha.5 : propager source complete pour permettre fetch async
235
+ // universel (geojson_path, geojson_url, wfs, boundary_admin, grist_table, x_*).
236
+ // Legacy V1.13 catalog_layers.geojson reste pour compat (source.type='geojson' inline).
234
237
  const geojson = l.source && l.source.type === "geojson" ? l.source.data : null;
235
238
  const style = l.style || {};
236
239
  const interactions = l.interactions || {};
@@ -240,17 +243,29 @@ function _v031ToLegacyShim(params, hostContext) {
240
243
  name: l.name,
241
244
  geometry_type: l.geometry_type,
242
245
  geojson,
246
+ source_v031: l.source,
243
247
  n_features: l.n_features,
244
248
  bbox: l.bbox,
245
249
  classification: classifShim,
246
250
  outline: style.outline,
247
251
  hollow_point: style.hollow_point
248
252
  });
253
+ // V0.3.1 opacity.reactive (Sprint V0.2 alpha.4, 2026-07-10) : preserver
254
+ // la config fade progressif pour applyBinding("time") au lieu du filter
255
+ // binaire hide/show historique.
256
+ const opacityStatic = typeof style.opacity === "object"
257
+ ? (style.opacity.kind === "static" ? style.opacity.value : undefined)
258
+ : style.opacity;
259
+ const opacityReactive = (typeof style.opacity === "object"
260
+ && style.opacity.kind === "reactive")
261
+ ? style.opacity
262
+ : null;
249
263
  overrides.push({
250
264
  layer_id_ref: l.id,
251
265
  visible: style.visible !== false,
252
266
  z_index: style.z_index,
253
- opacity: typeof style.opacity === "object" ? undefined : style.opacity,
267
+ opacity: opacityStatic,
268
+ opacity_reactive: opacityReactive,
254
269
  classification: classifShim,
255
270
  outline: style.outline,
256
271
  hollow_point: style.hollow_point,
@@ -267,6 +282,108 @@ function _v031ToLegacyShim(params, hostContext) {
267
282
  return { params: shimParams, hostContext: shimHost };
268
283
  }
269
284
 
285
+ // V0.3.2 alpha.5 (Sprint V0.2 session finale, 2026-07-11) : resolveur
286
+ // universel des 7 types de source du contract V0.3.2. Retourne une Promise
287
+ // qui resout vers un GeoJSON FeatureCollection utilisable par MapLibre.
288
+ // Pattern async : le layer est ajoute immediatement avec source vide, puis
289
+ // setData appele quand la promise resout. Consequence : la carte s'affiche
290
+ // instantanement (fond + controls), features apparaissent async.
291
+ //
292
+ // Types supportes :
293
+ // - geojson → source.data (immediat)
294
+ // - geojson_path → fetch(path, {credentials:'include'}) same-origin
295
+ // - geojson_url → fetch(url) public sans credentials
296
+ // - wfs → construct WFS GetFeature URL + fetch
297
+ // - boundary_admin → hostContext.boundary_resolver({catalog_id, filter})
298
+ // - grist_table → hostContext.grist_resolver({table, id_field, sync})
299
+ // - x_<prefix> → hostContext.custom_resolvers[prefix]
300
+ // - vector/pmtiles → PAS gere ici (utiliser addSource type=vector direct)
301
+ async function _fetchSourceData(source, hostContext) {
302
+ if (!source || !source.type) return { type: "FeatureCollection", features: [] };
303
+ const t = source.type;
304
+
305
+ if (t === "geojson") {
306
+ if (typeof source.data === "string") {
307
+ // legacy : data peut etre une URL string
308
+ const resp = await fetch(source.data);
309
+ if (!resp.ok) throw new Error(`geojson data fetch failed: ${resp.status}`);
310
+ return resp.json();
311
+ }
312
+ return source.data || { type: "FeatureCollection", features: [] };
313
+ }
314
+
315
+ if (t === "geojson_path") {
316
+ // Path serveur (PVC hub). Fetch same-origin avec cookies OIDC.
317
+ const url = source.fetch_url || source.path;
318
+ const resp = await fetch(url, { credentials: "include" });
319
+ if (!resp.ok) throw new Error(`geojson_path fetch failed: ${resp.status}`);
320
+ return resp.json();
321
+ }
322
+
323
+ if (t === "geojson_url") {
324
+ // URL publique. Fetch sans credentials.
325
+ const resp = await fetch(source.url);
326
+ if (!resp.ok) throw new Error(`geojson_url fetch failed: ${resp.status}`);
327
+ return resp.json();
328
+ }
329
+
330
+ if (t === "wfs") {
331
+ // Construct GetFeature URL avec parametres par defaut V0.3.2.
332
+ const p = new URLSearchParams({
333
+ service: "WFS",
334
+ version: "2.0.0",
335
+ request: "GetFeature",
336
+ typeName: source.type_name,
337
+ outputFormat: "application/json",
338
+ srsName: "EPSG:4326",
339
+ });
340
+ if (source.max_features) p.set("count", String(source.max_features));
341
+ if (source.filter_cql) p.set("cql_filter", source.filter_cql);
342
+ const sep = source.url.includes("?") ? "&" : "?";
343
+ const resp = await fetch(source.url + sep + p.toString());
344
+ if (!resp.ok) throw new Error(`wfs fetch failed: ${resp.status}`);
345
+ return resp.json();
346
+ }
347
+
348
+ if (t === "boundary_admin") {
349
+ // Delegue au hostContext du consommateur.
350
+ const resolver = hostContext && hostContext.boundary_resolver;
351
+ if (typeof resolver !== "function") {
352
+ throw new Error(
353
+ "boundary_admin requires hostContext.boundary_resolver(source)"
354
+ );
355
+ }
356
+ return await resolver(source);
357
+ }
358
+
359
+ if (t === "grist_table") {
360
+ // Delegue au widget Grist parent.
361
+ const resolver = hostContext && hostContext.grist_resolver;
362
+ if (typeof resolver !== "function") {
363
+ throw new Error(
364
+ "grist_table requires hostContext.grist_resolver(source)"
365
+ );
366
+ }
367
+ return await resolver(source);
368
+ }
369
+
370
+ if (typeof t === "string" && t.startsWith("x_")) {
371
+ // Extension par-projet.
372
+ const resolvers = (hostContext && hostContext.custom_resolvers) || {};
373
+ const resolver = resolvers[t];
374
+ if (typeof resolver !== "function") {
375
+ throw new Error(
376
+ `custom source type '${t}' requires hostContext.custom_resolvers['${t}']`
377
+ );
378
+ }
379
+ return await resolver(source);
380
+ }
381
+
382
+ // Types vector/pmtiles/... : geres directement par MapLibre addSource,
383
+ // pas via ce resolveur.
384
+ throw new Error(`_fetchSourceData: type '${t}' non supporte comme geojson`);
385
+ }
386
+
270
387
  // V0.3.1 classification : {color: {mode, field, method, breaks, palette,
271
388
  // _compiled_expression}, size, label} -> {paint_expression, method, field, ...}
272
389
  // attendus par _paintForClassification (V1.13).
@@ -292,6 +409,15 @@ function _v031ClassificationShim(classif) {
292
409
  return out;
293
410
  }
294
411
 
412
+ // V0.3.1 alpha.4 : normalise une position (top-left/bottom-right/...) vers
413
+ // les 4 positions acceptees par MapLibre. Fallback sur defaut si absente ou
414
+ // invalide (avoid warning MapLibre "Position undefined").
415
+ function _mlPosition(pos, fallback) {
416
+ const valid = ["top-left", "top-right", "bottom-left", "bottom-right"];
417
+ if (typeof pos === "string" && valid.includes(pos)) return pos;
418
+ return fallback;
419
+ }
420
+
295
421
  /**
296
422
  * Résout la zone d'étude en {center, zoom, bbox} exploitables par MapLibre.
297
423
  * Supporte les 3 modes du contrat V1.13 : commune | manual | study.
@@ -1090,13 +1216,37 @@ export class GeoMap extends HTMLElement {
1090
1216
  center: zone.center,
1091
1217
  zoom: zone.zoom,
1092
1218
  });
1093
- this.map.addControl(new ml.NavigationControl({ showCompass: false }), "top-left");
1219
+
1220
+ // V0.3.1 alpha.4 : declaratifs params.scalebar / params.north_arrow.
1221
+ // Sans north_arrow declare, on garde le comportement historique
1222
+ // NavigationControl sans compass. Avec, on affiche compass a la position
1223
+ // demandee. scalebar => ScaleControl natif MapLibre a la position/unit.
1224
+ const northCfg = params.north_arrow;
1225
+ const scaleCfg = params.scalebar;
1226
+ const showCompass = !!northCfg;
1227
+ const northPos = northCfg && northCfg.position ? northCfg.position : "top-left";
1228
+ this.map.addControl(
1229
+ new ml.NavigationControl({ showCompass, visualizePitch: showCompass }),
1230
+ _mlPosition(northPos, "top-left"),
1231
+ );
1232
+ if (scaleCfg) {
1233
+ const unit = scaleCfg.unit === "imperial" ? "imperial" : "metric";
1234
+ const scalePos = _mlPosition(scaleCfg.position, "bottom-right");
1235
+ this.map.addControl(
1236
+ new ml.ScaleControl({ maxWidth: 120, unit }),
1237
+ scalePos,
1238
+ );
1239
+ }
1094
1240
 
1095
1241
  const layersOverride = params.layers_override || [];
1096
1242
  const catalogLayers =
1097
1243
  this._hostContext.catalog_layers || params._catalog_layers || [];
1098
1244
 
1099
1245
  this._layerIds = [];
1246
+ // V0.3.1 (Sprint V0.2 alpha.4) : cache config opacity.reactive par layerId
1247
+ // pour permettre au binding 'time' de faire un fade progressif au lieu
1248
+ // d'un setFilter binaire hide/show. Cle = layer id MapLibre applique.
1249
+ this._reactiveOpacity = {};
1100
1250
 
1101
1251
  // P2 contract-drift V1.13 (review 2026-07-08) : respecter LayerOverride.z_index.
1102
1252
  // Le contrat V1.13 declare z_index (int) pour reordonner les layers a
@@ -1130,6 +1280,40 @@ export class GeoMap extends HTMLElement {
1130
1280
  data: sceneLayer.geojson || { type: "FeatureCollection", features: [] },
1131
1281
  });
1132
1282
 
1283
+ // V0.3.2 alpha.5 : fetch async des sources non-inline
1284
+ // (geojson_path, geojson_url, wfs, boundary_admin, grist_table, x_*).
1285
+ // La carte s'affiche instantanement, features arrivent async.
1286
+ if (sceneLayer.source_v031
1287
+ && sceneLayer.source_v031.type !== "geojson"
1288
+ && !sceneLayer.geojson) {
1289
+ const hostCtx = this._hostContext || {};
1290
+ _fetchSourceData(sceneLayer.source_v031, hostCtx)
1291
+ .then((geojson) => {
1292
+ const src = this.map.getSource(sourceId);
1293
+ if (src && typeof src.setData === "function") {
1294
+ src.setData(geojson);
1295
+ }
1296
+ this.dispatchEvent(new CustomEvent("geo:source-loaded", {
1297
+ detail: {
1298
+ layerId: sceneLayer.id,
1299
+ features: (geojson.features || []).length,
1300
+ },
1301
+ }));
1302
+ })
1303
+ .catch((e) => {
1304
+ console.warn(
1305
+ "[geo-components] fetch source failed for",
1306
+ sceneLayer.id, "->", sceneLayer.source_v031, e
1307
+ );
1308
+ this.dispatchEvent(new CustomEvent("geo:source-error", {
1309
+ detail: {
1310
+ layerId: sceneLayer.id,
1311
+ error: String(e).slice(0, 200),
1312
+ },
1313
+ }));
1314
+ });
1315
+ }
1316
+
1133
1317
  // V0.2.0 Chantier 4 : détection type + auto-split polygon+point
1134
1318
  // Analyse la geojson pour détecter FC hétérogène et éviter le bug
1135
1319
  // POINT invisibles (remontée cerema-livrables).
@@ -1182,6 +1366,12 @@ export class GeoMap extends HTMLElement {
1182
1366
  this.map.addLayer(layerDef);
1183
1367
  this._layerIds.push(layerDef.id);
1184
1368
 
1369
+ // V0.3.1 alpha.4 : memoriser config opacity.reactive pour fade
1370
+ // progressif via applyBinding('time').
1371
+ if (styleOverride.opacity_reactive) {
1372
+ this._reactiveOpacity[layerDef.id] = styleOverride.opacity_reactive;
1373
+ }
1374
+
1185
1375
  // V0.3.1 style.outline : ajout automatique layer 'line' sur même source
1186
1376
  if (
1187
1377
  styleOverride.outline &&
@@ -1209,6 +1399,19 @@ export class GeoMap extends HTMLElement {
1209
1399
  }
1210
1400
  });
1211
1401
 
1402
+ // V0.3.1 alpha.4 : legend.mode='auto' + from_layer -> derive chips
1403
+ // depuis classification._compiled_expression du layer designe et
1404
+ // injecte une <geo-legend> auto-generee dans le shadow DOM du <geo-map>.
1405
+ const legendCfg = params.legend;
1406
+ if (legendCfg && (legendCfg.mode === "auto" || legendCfg.items)) {
1407
+ const items = Array.isArray(legendCfg.items)
1408
+ ? legendCfg.items
1409
+ : this._autoLegendItems(legendCfg, catalogLayers, orderedLayers);
1410
+ if (items && items.length) {
1411
+ this._injectLegend(legendCfg, items);
1412
+ }
1413
+ }
1414
+
1212
1415
  // fitBounds auto sur zone.bbox ou premier layer avec features
1213
1416
  if (zone.bbox) {
1214
1417
  this.map.fitBounds(zone.bbox, { padding: 40, maxZoom: 16 });
@@ -1378,6 +1581,89 @@ export class GeoMap extends HTMLElement {
1378
1581
  /**
1379
1582
  * Point d'entrée de l'orchestrateur bindings. Applique un binding reçu.
1380
1583
  */
1584
+ // V0.3.1 alpha.4 : derive items de legende auto depuis
1585
+ // classification._compiled_expression du layer from_layer.
1586
+ _autoLegendItems(legendCfg, catalogLayers, orderedLayers) {
1587
+ const fromId = legendCfg.from_layer;
1588
+ if (!fromId) return null;
1589
+ const target = orderedLayers.find(o => o.scene && o.scene.id === fromId);
1590
+ if (!target) return null;
1591
+ const classif = (target.override && target.override.classification)
1592
+ || target.scene.classification;
1593
+ if (!classif) return null;
1594
+ // Support 2 formats : V1.13 paint_expression direct, ou V0.3.1 shimme
1595
+ const expr = classif.paint_expression;
1596
+ if (!Array.isArray(expr) || expr.length < 3) return null;
1597
+ const items = [];
1598
+ if (expr[0] === "step") {
1599
+ // ["step", ["get", field], color0, threshold1, color1, threshold2, color2, ...]
1600
+ const firstColor = expr[2];
1601
+ items.push({ label: "avant " + expr[3], color: firstColor });
1602
+ for (let i = 3; i + 1 < expr.length; i += 2) {
1603
+ const thr = expr[i];
1604
+ const col = expr[i + 1];
1605
+ const next = expr[i + 2];
1606
+ const label = (i + 2 < expr.length)
1607
+ ? thr + "-" + next
1608
+ : "≥ " + thr;
1609
+ items.push({ label, color: col });
1610
+ }
1611
+ } else if (expr[0] === "match") {
1612
+ // ["match", ["get", field], v1, c1, v2, c2, ..., fallback]
1613
+ for (let i = 2; i + 1 < expr.length; i += 2) {
1614
+ items.push({ label: String(expr[i]), color: expr[i + 1] });
1615
+ }
1616
+ }
1617
+ return items;
1618
+ }
1619
+
1620
+ // V0.3.1 alpha.4 : injecte une legende auto-generee dans le shadow DOM.
1621
+ // Utilise le meme rendering que <geo-legend> mais integre inline.
1622
+ _injectLegend(legendCfg, items) {
1623
+ const shadow = this.shadowRoot;
1624
+ if (!shadow) return;
1625
+ const pos = legendCfg.position || "bottom-left";
1626
+ const title = legendCfg.title || "Légende";
1627
+ const wrap = document.createElement("div");
1628
+ wrap.className = "gc-legend";
1629
+ const posStyles = {
1630
+ "top-left": "top:8px;left:8px;",
1631
+ "top-right": "top:8px;right:8px;",
1632
+ "bottom-left": "bottom:8px;left:8px;",
1633
+ "bottom-right": "bottom:44px;right:8px;",
1634
+ };
1635
+ const styleAttr = posStyles[pos] || posStyles["bottom-left"];
1636
+ wrap.setAttribute("style",
1637
+ "position:absolute;" + styleAttr +
1638
+ "background:rgba(255,255,255,0.94);padding:8px 12px;border-radius:4px;" +
1639
+ "box-shadow:0 1px 4px rgba(0,0,0,0.15);font-family:Marianne,system-ui,sans-serif;" +
1640
+ "font-size:11px;color:#333;z-index:5;max-width:260px;"
1641
+ );
1642
+ const t = document.createElement("div");
1643
+ t.setAttribute("style",
1644
+ "color:#000091;text-transform:uppercase;font-size:10px;letter-spacing:0.5px;" +
1645
+ "font-weight:700;margin-bottom:6px;");
1646
+ t.textContent = title;
1647
+ wrap.appendChild(t);
1648
+ for (const it of items) {
1649
+ const chip = document.createElement("div");
1650
+ chip.setAttribute("style",
1651
+ "display:flex;align-items:center;gap:6px;margin:2px 0;");
1652
+ const sw = document.createElement("span");
1653
+ sw.setAttribute("style",
1654
+ "width:14px;height:10px;background:" + it.color + ";" +
1655
+ "border:1px solid rgba(0,0,0,0.15);border-radius:2px;flex-shrink:0;");
1656
+ chip.appendChild(sw);
1657
+ const lbl = document.createElement("span");
1658
+ lbl.textContent = it.label;
1659
+ chip.appendChild(lbl);
1660
+ wrap.appendChild(chip);
1661
+ }
1662
+ // Injecter dans .gc-map pour que la legende suive la carte
1663
+ const mapDiv = shadow.querySelector(".gc-map");
1664
+ if (mapDiv) mapDiv.appendChild(wrap);
1665
+ }
1666
+
1381
1667
  applyBinding(detail) {
1382
1668
  const { prop, value } = detail || {};
1383
1669
  if (!this.map || !prop) return;
@@ -1385,11 +1671,47 @@ export class GeoMap extends HTMLElement {
1385
1671
 
1386
1672
  switch (prop) {
1387
1673
  case "time": {
1388
- // Filtre temporel : ['<=', ['get', 'annee'], year]
1674
+ // V0.3.1 alpha.4 : si le layer a opacity.reactive prop=time, on
1675
+ // applique un fade progressif (matched/before/after/null_value)
1676
+ // au lieu du filter binaire hide/show V1.13.
1389
1677
  const year = _asYear(value);
1390
- if (year != null && primaryLayer) {
1391
- const timeField = detail.field || "annee";
1392
- this.map.setFilter(primaryLayer, ["<=", ["get", timeField], year]);
1678
+ if (year == null || !primaryLayer) break;
1679
+ const timeField = detail.field || "annee";
1680
+
1681
+ for (const lid of this._layerIds) {
1682
+ const cfg = this._reactiveOpacity[lid];
1683
+ if (cfg && cfg.prop === "time") {
1684
+ // Fade progressif : expression case selon (field vs value)
1685
+ const field = cfg.field || timeField;
1686
+ const matched = cfg.matched ?? 0.85;
1687
+ const before = cfg.before ?? 0.85;
1688
+ const after = cfg.after ?? 0.12;
1689
+ const nullValue = cfg.null_value ?? 0.35;
1690
+ const expr = [
1691
+ "case",
1692
+ ["==", ["typeof", ["get", field]], "null"], nullValue,
1693
+ ["<", ["to-number", ["get", field]], year], before,
1694
+ [">", ["to-number", ["get", field]], year], after,
1695
+ matched,
1696
+ ];
1697
+ const layer = this.map.getLayer(lid);
1698
+ if (!layer) continue;
1699
+ const paintKey =
1700
+ layer.type === "fill" ? "fill-opacity" :
1701
+ layer.type === "line" ? "line-opacity" :
1702
+ layer.type === "circle" ? "circle-opacity" :
1703
+ layer.type === "fill-extrusion" ? "fill-extrusion-opacity" :
1704
+ null;
1705
+ if (paintKey) {
1706
+ try { this.map.setPaintProperty(lid, paintKey, expr); }
1707
+ catch (e) { /* pas critique */ }
1708
+ }
1709
+ } else if (lid === primaryLayer) {
1710
+ // Fallback V1.13 : filter binaire sur le layer primaire
1711
+ try {
1712
+ this.map.setFilter(lid, ["<=", ["get", timeField], year]);
1713
+ } catch (e) { /* ignore */ }
1714
+ }
1393
1715
  }
1394
1716
  break;
1395
1717
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passerelle-geo-components",
3
- "version": "0.2.0-alpha.3",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "Web Components pour cartes MapLibre + composants controllers pilotes - brique commune multi-projet avec contract SceneManifest V0.3.1. Zero dependance runtime, ES module, encapsulation Shadow DOM.",
5
5
  "type": "module",
6
6
  "main": "geo-components.js",
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: scene_manifest.schema.json
3
- # timestamp: 2026-07-10T13:35:15+00:00
3
+ # timestamp: 2026-07-11T12:39:43+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -337,6 +337,18 @@ class LayerSource2(BaseModel):
337
337
 
338
338
 
339
339
  class LayerSource3(BaseModel):
340
+ model_config = ConfigDict(
341
+ extra="allow",
342
+ )
343
+ type: Literal["geojson_url"]
344
+ url: AnyUrl = Field(
345
+ ...,
346
+ description="URL publique du GeoJSON (S3 public, CDN, endpoint sans auth). Fetche cote client sans credentials.",
347
+ )
348
+ crs: Literal["EPSG:4326"] = "EPSG:4326"
349
+
350
+
351
+ class LayerSource4(BaseModel):
340
352
  model_config = ConfigDict(
341
353
  extra="allow",
342
354
  )
@@ -350,7 +362,7 @@ class LayerSource3(BaseModel):
350
362
  promote_id: str | None = None
351
363
 
352
364
 
353
- class LayerSource4(BaseModel):
365
+ class LayerSource5(BaseModel):
354
366
  model_config = ConfigDict(
355
367
  extra="allow",
356
368
  )
@@ -369,7 +381,7 @@ class Sync(Enum):
369
381
  save = "save"
370
382
 
371
383
 
372
- class LayerSource5(BaseModel):
384
+ class LayerSource6(BaseModel):
373
385
  model_config = ConfigDict(
374
386
  extra="allow",
375
387
  )
@@ -382,7 +394,7 @@ class LayerSource5(BaseModel):
382
394
  )
383
395
 
384
396
 
385
- class LayerSource6(BaseModel):
397
+ class LayerSource7(BaseModel):
386
398
  model_config = ConfigDict(
387
399
  extra="allow",
388
400
  )
@@ -391,7 +403,7 @@ class LayerSource6(BaseModel):
391
403
  type_name: str = Field(..., description="WFS TypeName (ex: BATIMENT.BATIMENT).")
392
404
 
393
405
 
394
- class LayerSource7(BaseModel):
406
+ class LayerSource8(BaseModel):
395
407
  model_config = ConfigDict(
396
408
  extra="allow",
397
409
  )
@@ -405,7 +417,7 @@ class LayerSource7(BaseModel):
405
417
  )
406
418
 
407
419
 
408
- class LayerSource8(BaseModel):
420
+ class LayerSource9(BaseModel):
409
421
  model_config = ConfigDict(
410
422
  extra="allow",
411
423
  )
@@ -424,9 +436,10 @@ class LayerSource(
424
436
  | LayerSource6
425
437
  | LayerSource7
426
438
  | LayerSource8
439
+ | LayerSource9
427
440
  ]
428
441
  ):
429
- root: LayerSource1 | LayerSource2 | LayerSource3 | LayerSource4 | LayerSource5 | LayerSource6 | LayerSource7 | LayerSource8 = Field(
442
+ root: LayerSource1 | LayerSource2 | LayerSource3 | LayerSource4 | LayerSource5 | LayerSource6 | LayerSource7 | LayerSource8 | LayerSource9 = Field(
430
443
  ...,
431
444
  description="Source de données. Discriminated union sur 'type'. Alignée MapLibre style spec (v8+).",
432
445
  )
@@ -912,7 +925,7 @@ class Layer(BaseModel):
912
925
  extensions: dict[constr(pattern=r"^[a-z][a-z_0-9]*$"), dict[str, Any]] | None = None
913
926
 
914
927
 
915
- class ScenemanifestV031(BaseModel):
928
+ class ScenemanifestV032(BaseModel):
916
929
  model_config = ConfigDict(
917
930
  extra="forbid",
918
931
  )
@@ -1,7 +1,7 @@
1
1
  {
2
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",
3
+ "$id": "https://nic01asfr.github.io/Passerelle/schemas/scene_manifest/0.3.2.json",
4
+ "title": "SceneManifest V0.3.2",
5
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
6
  "type": "object",
7
7
  "required": [
@@ -442,6 +442,16 @@
442
442
  },
443
443
  "additionalProperties": true
444
444
  },
445
+ {
446
+ "type": "object",
447
+ "required": ["type", "url"],
448
+ "properties": {
449
+ "type": { "const": "geojson_url" },
450
+ "url": { "type": "string", "format": "uri", "description": "URL publique du GeoJSON (S3 public, CDN, endpoint sans auth). Fetche cote client sans credentials." },
451
+ "crs": { "type": "string", "const": "EPSG:4326", "default": "EPSG:4326" }
452
+ },
453
+ "additionalProperties": true
454
+ },
445
455
  {
446
456
  "type": "object",
447
457
  "required": ["type", "url", "source_layer"],