|
334 | 334 | setCoord(ll.lat, ll.lng); |
335 | 335 | }); |
336 | 336 | map.on("click", function (e) { |
| 337 | + // A click on a building footprint selects that building (the layer's own |
| 338 | + // handler). It must not also drag the site pin there and silently |
| 339 | + // rewrite the saved coordinates. |
| 340 | + if (map.getLayer("roof-buildings-fill") && |
| 341 | + map.queryRenderedFeatures(e.point, { layers: ["roof-buildings-fill"] }).length) { |
| 342 | + return; |
| 343 | + } |
337 | 344 | marker.setLngLat(e.lngLat); |
338 | 345 | setCoord(e.lngLat.lat, e.lngLat.lng); |
339 | 346 | }); |
|
556 | 563 | roofSay("Found " + roofState.features.length + |
557 | 564 | " building(s). Pick yours on the map or in the list."); |
558 | 565 | drawBuildings(); |
| 566 | + fitToBuildings(d.latitude, d.longitude); |
559 | 567 | renderBuildingList(ctx); |
| 568 | + var mapEl = document.getElementById("weather-map"); |
| 569 | + if (mapEl && mapEl.scrollIntoView) mapEl.scrollIntoView({ block: "nearest" }); |
560 | 570 | }) |
561 | 571 | .catch(function (e) { roofSay(ctx.escHtml(String(e && e.message || e)), "bad"); }); |
562 | 572 | } |
|
574 | 584 |
|
575 | 585 | // MapLibre paints with concrete colours and cannot read var(), the same |
576 | 586 | // problem the canvas charts have. Resolve the theme tokens through a hidden |
577 | | - // probe that inherits :root, exactly as app.js's cssColor does. Resolved once |
578 | | - // per layer creation, so a theme toggle mid-pick keeps the old hue until the |
579 | | - // tab is reopened — the footprints stay legible either way. |
| 587 | + // probe that inherits :root, exactly as app.js's cssColor does. The theme |
| 588 | + // authors its tokens in oklch(), which getComputedStyle passes through |
| 589 | + // verbatim and MapLibre's parser rejects — so bake the resolved colour to |
| 590 | + // sRGB bytes through a 1x1 canvas, whose getImageData is sRGB by contract. |
| 591 | + // Resolved once per layer creation, so a theme toggle mid-pick keeps the old |
| 592 | + // hue until the tab is reopened — the footprints stay legible either way. |
580 | 593 | var _probe = null; |
581 | 594 | function themeColor(name, fallback) { |
582 | 595 | if (!_probe) { |
|
585 | 598 | document.body.appendChild(_probe); |
586 | 599 | } |
587 | 600 | _probe.style.color = "var(" + name + ", " + fallback + ")"; |
588 | | - return getComputedStyle(_probe).color || fallback; |
| 601 | + var resolved = getComputedStyle(_probe).color || fallback; |
| 602 | + try { |
| 603 | + var ctx = document.createElement("canvas").getContext("2d", { willReadFrequently: true }); |
| 604 | + ctx.fillStyle = fallback; // an unparseable resolved value leaves this |
| 605 | + ctx.fillStyle = resolved; |
| 606 | + ctx.fillRect(0, 0, 1, 1); |
| 607 | + var px = ctx.getImageData(0, 0, 1, 1).data; |
| 608 | + return "rgb(" + px[0] + "," + px[1] + "," + px[2] + ")"; |
| 609 | + } catch (e) { |
| 610 | + return fallback; |
| 611 | + } |
| 612 | + } |
| 613 | + |
| 614 | + // A "case" condition must be a typed boolean: a bare ["get", ...] is value- |
| 615 | + // typed, and MapLibre rejects the whole layer through its error event without |
| 616 | + // throwing — the picker then looks enabled while the map stays empty. |
| 617 | + function whenSelected(then, otherwise) { |
| 618 | + return ["case", ["boolean", ["get", "selected"], false], then, otherwise]; |
589 | 619 | } |
590 | 620 |
|
591 | 621 | function drawBuildings() { |
592 | 622 | var map = window._weatherMap; |
593 | | - if (!map || !map.isStyleLoaded || !map.isStyleLoaded()) return; |
| 623 | + if (!map) return; |
| 624 | + if (!map.isStyleLoaded || !map.isStyleLoaded()) { |
| 625 | + // A find can win the race against the style. Idempotent, so a stacked |
| 626 | + // retry only costs a setData with identical data. |
| 627 | + map.once("load", drawBuildings); |
| 628 | + return; |
| 629 | + } |
594 | 630 | var data = featureCollection(); |
595 | 631 | var src = map.getSource("roof-buildings"); |
596 | 632 | if (src) { src.setData(data); return; } |
|
600 | 636 | map.addLayer({ |
601 | 637 | id: "roof-buildings-fill", type: "fill", source: "roof-buildings", |
602 | 638 | paint: { |
603 | | - "fill-color": ["case", ["get", "selected"], picked, candidate], |
604 | | - "fill-opacity": ["case", ["get", "selected"], 0.55, 0.25], |
| 639 | + "fill-color": whenSelected(picked, candidate), |
| 640 | + "fill-opacity": whenSelected(0.55, 0.25), |
605 | 641 | }, |
606 | 642 | }); |
607 | 643 | map.addLayer({ |
608 | 644 | id: "roof-buildings-line", type: "line", source: "roof-buildings", |
609 | 645 | paint: { |
610 | | - "line-color": ["case", ["get", "selected"], picked, candidate], |
611 | | - "line-width": ["case", ["get", "selected"], 2.5, 1], |
| 646 | + "line-color": whenSelected(picked, candidate), |
| 647 | + "line-width": whenSelected(2.5, 1), |
612 | 648 | }, |
613 | 649 | }); |
614 | 650 | map.on("click", "roof-buildings-fill", function (e) { |
|
624 | 660 | }); |
625 | 661 | } |
626 | 662 |
|
| 663 | + // [[west, south], [east, north]] around the buildings someone would actually |
| 664 | + // pick — the nearby ones the list also shows — plus the site pin. Fitting |
| 665 | + // the whole search radius leaves every footprint a few pixels wide. |
| 666 | + function buildingsBounds(features, siteLat, siteLon) { |
| 667 | + var west = null, south = null, east = null, north = null; |
| 668 | + function extend(lon, lat) { |
| 669 | + if (typeof lon !== "number" || typeof lat !== "number") return; |
| 670 | + if (west === null || lon < west) west = lon; |
| 671 | + if (east === null || lon > east) east = lon; |
| 672 | + if (south === null || lat < south) south = lat; |
| 673 | + if (north === null || lat > north) north = lat; |
| 674 | + } |
| 675 | + extend(siteLon, siteLat); |
| 676 | + var near = features.filter(function (f) { |
| 677 | + return ((f.properties || {}).distance_m || 0) <= 150; |
| 678 | + }); |
| 679 | + if (near.length < 3) near = features; |
| 680 | + near.forEach(function (f) { |
| 681 | + var rings = (f.geometry && f.geometry.coordinates) || []; |
| 682 | + (rings[0] || []).forEach(function (pt) { extend(pt[0], pt[1]); }); |
| 683 | + }); |
| 684 | + if (west === null) return null; |
| 685 | + return [[west, south], [east, north]]; |
| 686 | + } |
| 687 | + |
| 688 | + // The picker opens at city zoom, where a footprint is smaller than a pixel. |
| 689 | + // Zoom to the search results once per find; selection redraws leave the |
| 690 | + // camera where the operator put it. |
| 691 | + function fitToBuildings(siteLat, siteLon) { |
| 692 | + var map = window._weatherMap; |
| 693 | + if (!map || !roofState.features.length) return; |
| 694 | + var bounds = buildingsBounds(roofState.features, siteLat, siteLon); |
| 695 | + if (!bounds) return; |
| 696 | + map.fitBounds(bounds, { padding: 48, maxZoom: 17.5, duration: 600 }); |
| 697 | + } |
| 698 | + |
627 | 699 | function selectBuilding(id) { |
628 | 700 | roofState.selectedId = id; |
629 | 701 | drawBuildings(); |
|
823 | 895 | }, |
824 | 896 | }; |
825 | 897 |
|
826 | | - S.tabs.weather._pure = { arraysSummary: arraysSummary }; |
| 898 | + S.tabs.weather._pure = { |
| 899 | + arraysSummary: arraysSummary, |
| 900 | + whenSelected: whenSelected, |
| 901 | + buildingsBounds: buildingsBounds, |
| 902 | + }; |
827 | 903 | })(); |
0 commit comments