passerelle-geo-components 0.2.0-alpha.3 → 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 +188 -6
  2. package/package.json +1 -1
package/geo-components.js CHANGED
@@ -246,11 +246,22 @@ function _v031ToLegacyShim(params, hostContext) {
246
246
  outline: style.outline,
247
247
  hollow_point: style.hollow_point
248
248
  });
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;
249
259
  overrides.push({
250
260
  layer_id_ref: l.id,
251
261
  visible: style.visible !== false,
252
262
  z_index: style.z_index,
253
- opacity: typeof style.opacity === "object" ? undefined : style.opacity,
263
+ opacity: opacityStatic,
264
+ opacity_reactive: opacityReactive,
254
265
  classification: classifShim,
255
266
  outline: style.outline,
256
267
  hollow_point: style.hollow_point,
@@ -292,6 +303,15 @@ function _v031ClassificationShim(classif) {
292
303
  return out;
293
304
  }
294
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
+
295
315
  /**
296
316
  * Résout la zone d'étude en {center, zoom, bbox} exploitables par MapLibre.
297
317
  * Supporte les 3 modes du contrat V1.13 : commune | manual | study.
@@ -1090,13 +1110,37 @@ export class GeoMap extends HTMLElement {
1090
1110
  center: zone.center,
1091
1111
  zoom: zone.zoom,
1092
1112
  });
1093
- 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
+ }
1094
1134
 
1095
1135
  const layersOverride = params.layers_override || [];
1096
1136
  const catalogLayers =
1097
1137
  this._hostContext.catalog_layers || params._catalog_layers || [];
1098
1138
 
1099
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 = {};
1100
1144
 
1101
1145
  // P2 contract-drift V1.13 (review 2026-07-08) : respecter LayerOverride.z_index.
1102
1146
  // Le contrat V1.13 declare z_index (int) pour reordonner les layers a
@@ -1182,6 +1226,12 @@ export class GeoMap extends HTMLElement {
1182
1226
  this.map.addLayer(layerDef);
1183
1227
  this._layerIds.push(layerDef.id);
1184
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
+
1185
1235
  // V0.3.1 style.outline : ajout automatique layer 'line' sur même source
1186
1236
  if (
1187
1237
  styleOverride.outline &&
@@ -1209,6 +1259,19 @@ export class GeoMap extends HTMLElement {
1209
1259
  }
1210
1260
  });
1211
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
+
1212
1275
  // fitBounds auto sur zone.bbox ou premier layer avec features
1213
1276
  if (zone.bbox) {
1214
1277
  this.map.fitBounds(zone.bbox, { padding: 40, maxZoom: 16 });
@@ -1378,6 +1441,89 @@ export class GeoMap extends HTMLElement {
1378
1441
  /**
1379
1442
  * Point d'entrée de l'orchestrateur bindings. Applique un binding reçu.
1380
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
+
1381
1527
  applyBinding(detail) {
1382
1528
  const { prop, value } = detail || {};
1383
1529
  if (!this.map || !prop) return;
@@ -1385,11 +1531,47 @@ export class GeoMap extends HTMLElement {
1385
1531
 
1386
1532
  switch (prop) {
1387
1533
  case "time": {
1388
- // 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.
1389
1537
  const year = _asYear(value);
1390
- if (year != null && primaryLayer) {
1391
- const timeField = detail.field || "annee";
1392
- 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
+ }
1393
1575
  }
1394
1576
  break;
1395
1577
  }
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.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",