passerelle-geo-components 0.2.0-alpha.2 → 0.2.0-alpha.4

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.
Files changed (2) hide show
  1. package/geo-components.js +220 -12
  2. package/package.json +1 -1
package/geo-components.js CHANGED
@@ -232,6 +232,9 @@ function _v031ToLegacyShim(params, hostContext) {
232
232
  const overrides = [];
233
233
  for (const l of params.layers) {
234
234
  const geojson = l.source && l.source.type === "geojson" ? l.source.data : null;
235
+ const style = l.style || {};
236
+ const interactions = l.interactions || {};
237
+ const classifShim = _v031ClassificationShim(style.classification);
235
238
  catalog.push({
236
239
  id: l.id,
237
240
  name: l.name,
@@ -239,18 +242,27 @@ function _v031ToLegacyShim(params, hostContext) {
239
242
  geojson,
240
243
  n_features: l.n_features,
241
244
  bbox: l.bbox,
242
- classification: l.style && l.style.classification,
243
- outline: l.style && l.style.outline,
244
- hollow_point: l.style && l.style.hollow_point
245
+ classification: classifShim,
246
+ outline: style.outline,
247
+ hollow_point: style.hollow_point
245
248
  });
246
- const style = l.style || {};
247
- const interactions = l.interactions || {};
249
+ // V0.3.1 opacity.reactive (Sprint V0.2 alpha.4, 2026-07-10) : preserver
250
+ // la config fade progressif pour applyBinding("time") au lieu du filter
251
+ // binaire hide/show historique.
252
+ const opacityStatic = typeof style.opacity === "object"
253
+ ? (style.opacity.kind === "static" ? style.opacity.value : undefined)
254
+ : style.opacity;
255
+ const opacityReactive = (typeof style.opacity === "object"
256
+ && style.opacity.kind === "reactive")
257
+ ? style.opacity
258
+ : null;
248
259
  overrides.push({
249
260
  layer_id_ref: l.id,
250
261
  visible: style.visible !== false,
251
262
  z_index: style.z_index,
252
- opacity: typeof style.opacity === "object" ? undefined : style.opacity,
253
- classification: style.classification,
263
+ opacity: opacityStatic,
264
+ opacity_reactive: opacityReactive,
265
+ classification: classifShim,
254
266
  outline: style.outline,
255
267
  hollow_point: style.hollow_point,
256
268
  popup_template: interactions.popup_template,
@@ -266,6 +278,40 @@ function _v031ToLegacyShim(params, hostContext) {
266
278
  return { params: shimParams, hostContext: shimHost };
267
279
  }
268
280
 
281
+ // V0.3.1 classification : {color: {mode, field, method, breaks, palette,
282
+ // _compiled_expression}, size, label} -> {paint_expression, method, field, ...}
283
+ // attendus par _paintForClassification (V1.13).
284
+ function _v031ClassificationShim(classif) {
285
+ if (!classif) return null;
286
+ // Deja au format V1.13 (paint_expression direct) -> passthrough
287
+ if (classif.paint_expression) return classif;
288
+ // V0.3.1 nested {color: {...}}
289
+ const color = classif.color || classif;
290
+ if (!color || !color.mode) return classif;
291
+ const out = {
292
+ method: color.method || color.mode,
293
+ field: color.field
294
+ };
295
+ // Priorite : expression MapLibre pre-compilee dans _compiled_expression
296
+ if (Array.isArray(color._compiled_expression)) {
297
+ out.paint_expression = color._compiled_expression;
298
+ } else if (color.mode === "single" && color.value) {
299
+ out.paint_expression = color.value;
300
+ } else if (color.mode === "expression" && Array.isArray(color.expression)) {
301
+ out.paint_expression = color.expression;
302
+ }
303
+ return out;
304
+ }
305
+
306
+ // V0.3.1 alpha.4 : normalise une position (top-left/bottom-right/...) vers
307
+ // les 4 positions acceptees par MapLibre. Fallback sur defaut si absente ou
308
+ // invalide (avoid warning MapLibre "Position undefined").
309
+ function _mlPosition(pos, fallback) {
310
+ const valid = ["top-left", "top-right", "bottom-left", "bottom-right"];
311
+ if (typeof pos === "string" && valid.includes(pos)) return pos;
312
+ return fallback;
313
+ }
314
+
269
315
  /**
270
316
  * Résout la zone d'étude en {center, zoom, bbox} exploitables par MapLibre.
271
317
  * Supporte les 3 modes du contrat V1.13 : commune | manual | study.
@@ -1064,13 +1110,37 @@ export class GeoMap extends HTMLElement {
1064
1110
  center: zone.center,
1065
1111
  zoom: zone.zoom,
1066
1112
  });
1067
- this.map.addControl(new ml.NavigationControl({ showCompass: false }), "top-left");
1113
+
1114
+ // V0.3.1 alpha.4 : declaratifs params.scalebar / params.north_arrow.
1115
+ // Sans north_arrow declare, on garde le comportement historique
1116
+ // NavigationControl sans compass. Avec, on affiche compass a la position
1117
+ // demandee. scalebar => ScaleControl natif MapLibre a la position/unit.
1118
+ const northCfg = params.north_arrow;
1119
+ const scaleCfg = params.scalebar;
1120
+ const showCompass = !!northCfg;
1121
+ const northPos = northCfg && northCfg.position ? northCfg.position : "top-left";
1122
+ this.map.addControl(
1123
+ new ml.NavigationControl({ showCompass, visualizePitch: showCompass }),
1124
+ _mlPosition(northPos, "top-left"),
1125
+ );
1126
+ if (scaleCfg) {
1127
+ const unit = scaleCfg.unit === "imperial" ? "imperial" : "metric";
1128
+ const scalePos = _mlPosition(scaleCfg.position, "bottom-right");
1129
+ this.map.addControl(
1130
+ new ml.ScaleControl({ maxWidth: 120, unit }),
1131
+ scalePos,
1132
+ );
1133
+ }
1068
1134
 
1069
1135
  const layersOverride = params.layers_override || [];
1070
1136
  const catalogLayers =
1071
1137
  this._hostContext.catalog_layers || params._catalog_layers || [];
1072
1138
 
1073
1139
  this._layerIds = [];
1140
+ // V0.3.1 (Sprint V0.2 alpha.4) : cache config opacity.reactive par layerId
1141
+ // pour permettre au binding 'time' de faire un fade progressif au lieu
1142
+ // d'un setFilter binaire hide/show. Cle = layer id MapLibre applique.
1143
+ this._reactiveOpacity = {};
1074
1144
 
1075
1145
  // P2 contract-drift V1.13 (review 2026-07-08) : respecter LayerOverride.z_index.
1076
1146
  // Le contrat V1.13 declare z_index (int) pour reordonner les layers a
@@ -1156,6 +1226,12 @@ export class GeoMap extends HTMLElement {
1156
1226
  this.map.addLayer(layerDef);
1157
1227
  this._layerIds.push(layerDef.id);
1158
1228
 
1229
+ // V0.3.1 alpha.4 : memoriser config opacity.reactive pour fade
1230
+ // progressif via applyBinding('time').
1231
+ if (styleOverride.opacity_reactive) {
1232
+ this._reactiveOpacity[layerDef.id] = styleOverride.opacity_reactive;
1233
+ }
1234
+
1159
1235
  // V0.3.1 style.outline : ajout automatique layer 'line' sur même source
1160
1236
  if (
1161
1237
  styleOverride.outline &&
@@ -1183,6 +1259,19 @@ export class GeoMap extends HTMLElement {
1183
1259
  }
1184
1260
  });
1185
1261
 
1262
+ // V0.3.1 alpha.4 : legend.mode='auto' + from_layer -> derive chips
1263
+ // depuis classification._compiled_expression du layer designe et
1264
+ // injecte une <geo-legend> auto-generee dans le shadow DOM du <geo-map>.
1265
+ const legendCfg = params.legend;
1266
+ if (legendCfg && (legendCfg.mode === "auto" || legendCfg.items)) {
1267
+ const items = Array.isArray(legendCfg.items)
1268
+ ? legendCfg.items
1269
+ : this._autoLegendItems(legendCfg, catalogLayers, orderedLayers);
1270
+ if (items && items.length) {
1271
+ this._injectLegend(legendCfg, items);
1272
+ }
1273
+ }
1274
+
1186
1275
  // fitBounds auto sur zone.bbox ou premier layer avec features
1187
1276
  if (zone.bbox) {
1188
1277
  this.map.fitBounds(zone.bbox, { padding: 40, maxZoom: 16 });
@@ -1352,6 +1441,89 @@ export class GeoMap extends HTMLElement {
1352
1441
  /**
1353
1442
  * Point d'entrée de l'orchestrateur bindings. Applique un binding reçu.
1354
1443
  */
1444
+ // V0.3.1 alpha.4 : derive items de legende auto depuis
1445
+ // classification._compiled_expression du layer from_layer.
1446
+ _autoLegendItems(legendCfg, catalogLayers, orderedLayers) {
1447
+ const fromId = legendCfg.from_layer;
1448
+ if (!fromId) return null;
1449
+ const target = orderedLayers.find(o => o.scene && o.scene.id === fromId);
1450
+ if (!target) return null;
1451
+ const classif = (target.override && target.override.classification)
1452
+ || target.scene.classification;
1453
+ if (!classif) return null;
1454
+ // Support 2 formats : V1.13 paint_expression direct, ou V0.3.1 shimme
1455
+ const expr = classif.paint_expression;
1456
+ if (!Array.isArray(expr) || expr.length < 3) return null;
1457
+ const items = [];
1458
+ if (expr[0] === "step") {
1459
+ // ["step", ["get", field], color0, threshold1, color1, threshold2, color2, ...]
1460
+ const firstColor = expr[2];
1461
+ items.push({ label: "avant " + expr[3], color: firstColor });
1462
+ for (let i = 3; i + 1 < expr.length; i += 2) {
1463
+ const thr = expr[i];
1464
+ const col = expr[i + 1];
1465
+ const next = expr[i + 2];
1466
+ const label = (i + 2 < expr.length)
1467
+ ? thr + "-" + next
1468
+ : "≥ " + thr;
1469
+ items.push({ label, color: col });
1470
+ }
1471
+ } else if (expr[0] === "match") {
1472
+ // ["match", ["get", field], v1, c1, v2, c2, ..., fallback]
1473
+ for (let i = 2; i + 1 < expr.length; i += 2) {
1474
+ items.push({ label: String(expr[i]), color: expr[i + 1] });
1475
+ }
1476
+ }
1477
+ return items;
1478
+ }
1479
+
1480
+ // V0.3.1 alpha.4 : injecte une legende auto-generee dans le shadow DOM.
1481
+ // Utilise le meme rendering que <geo-legend> mais integre inline.
1482
+ _injectLegend(legendCfg, items) {
1483
+ const shadow = this.shadowRoot;
1484
+ if (!shadow) return;
1485
+ const pos = legendCfg.position || "bottom-left";
1486
+ const title = legendCfg.title || "Légende";
1487
+ const wrap = document.createElement("div");
1488
+ wrap.className = "gc-legend";
1489
+ const posStyles = {
1490
+ "top-left": "top:8px;left:8px;",
1491
+ "top-right": "top:8px;right:8px;",
1492
+ "bottom-left": "bottom:8px;left:8px;",
1493
+ "bottom-right": "bottom:44px;right:8px;",
1494
+ };
1495
+ const styleAttr = posStyles[pos] || posStyles["bottom-left"];
1496
+ wrap.setAttribute("style",
1497
+ "position:absolute;" + styleAttr +
1498
+ "background:rgba(255,255,255,0.94);padding:8px 12px;border-radius:4px;" +
1499
+ "box-shadow:0 1px 4px rgba(0,0,0,0.15);font-family:Marianne,system-ui,sans-serif;" +
1500
+ "font-size:11px;color:#333;z-index:5;max-width:260px;"
1501
+ );
1502
+ const t = document.createElement("div");
1503
+ t.setAttribute("style",
1504
+ "color:#000091;text-transform:uppercase;font-size:10px;letter-spacing:0.5px;" +
1505
+ "font-weight:700;margin-bottom:6px;");
1506
+ t.textContent = title;
1507
+ wrap.appendChild(t);
1508
+ for (const it of items) {
1509
+ const chip = document.createElement("div");
1510
+ chip.setAttribute("style",
1511
+ "display:flex;align-items:center;gap:6px;margin:2px 0;");
1512
+ const sw = document.createElement("span");
1513
+ sw.setAttribute("style",
1514
+ "width:14px;height:10px;background:" + it.color + ";" +
1515
+ "border:1px solid rgba(0,0,0,0.15);border-radius:2px;flex-shrink:0;");
1516
+ chip.appendChild(sw);
1517
+ const lbl = document.createElement("span");
1518
+ lbl.textContent = it.label;
1519
+ chip.appendChild(lbl);
1520
+ wrap.appendChild(chip);
1521
+ }
1522
+ // Injecter dans .gc-map pour que la legende suive la carte
1523
+ const mapDiv = shadow.querySelector(".gc-map");
1524
+ if (mapDiv) mapDiv.appendChild(wrap);
1525
+ }
1526
+
1355
1527
  applyBinding(detail) {
1356
1528
  const { prop, value } = detail || {};
1357
1529
  if (!this.map || !prop) return;
@@ -1359,11 +1531,47 @@ export class GeoMap extends HTMLElement {
1359
1531
 
1360
1532
  switch (prop) {
1361
1533
  case "time": {
1362
- // Filtre temporel : ['<=', ['get', 'annee'], year]
1534
+ // V0.3.1 alpha.4 : si le layer a opacity.reactive prop=time, on
1535
+ // applique un fade progressif (matched/before/after/null_value)
1536
+ // au lieu du filter binaire hide/show V1.13.
1363
1537
  const year = _asYear(value);
1364
- if (year != null && primaryLayer) {
1365
- const timeField = detail.field || "annee";
1366
- this.map.setFilter(primaryLayer, ["<=", ["get", timeField], year]);
1538
+ if (year == null || !primaryLayer) break;
1539
+ const timeField = detail.field || "annee";
1540
+
1541
+ for (const lid of this._layerIds) {
1542
+ const cfg = this._reactiveOpacity[lid];
1543
+ if (cfg && cfg.prop === "time") {
1544
+ // Fade progressif : expression case selon (field vs value)
1545
+ const field = cfg.field || timeField;
1546
+ const matched = cfg.matched ?? 0.85;
1547
+ const before = cfg.before ?? 0.85;
1548
+ const after = cfg.after ?? 0.12;
1549
+ const nullValue = cfg.null_value ?? 0.35;
1550
+ const expr = [
1551
+ "case",
1552
+ ["==", ["typeof", ["get", field]], "null"], nullValue,
1553
+ ["<", ["to-number", ["get", field]], year], before,
1554
+ [">", ["to-number", ["get", field]], year], after,
1555
+ matched,
1556
+ ];
1557
+ const layer = this.map.getLayer(lid);
1558
+ if (!layer) continue;
1559
+ const paintKey =
1560
+ layer.type === "fill" ? "fill-opacity" :
1561
+ layer.type === "line" ? "line-opacity" :
1562
+ layer.type === "circle" ? "circle-opacity" :
1563
+ layer.type === "fill-extrusion" ? "fill-extrusion-opacity" :
1564
+ null;
1565
+ if (paintKey) {
1566
+ try { this.map.setPaintProperty(lid, paintKey, expr); }
1567
+ catch (e) { /* pas critique */ }
1568
+ }
1569
+ } else if (lid === primaryLayer) {
1570
+ // Fallback V1.13 : filter binaire sur le layer primaire
1571
+ try {
1572
+ this.map.setFilter(lid, ["<=", ["get", timeField], year]);
1573
+ } catch (e) { /* ignore */ }
1574
+ }
1367
1575
  }
1368
1576
  break;
1369
1577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passerelle-geo-components",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.4",
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",