Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
"leaflet.measure": "^1.0.0",
"leaflet.polylinemeasure": "^3.0.0",
"leaflet.vectorgrid": "^1.3.0",
"markerwithlabel": "^2.0.2",
"moment": "^2.29.1",
"nunjucks": "^3.2.4",
"remove": "^0.1.5",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/noop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Empty stub module used by webpack to replace packages that are incompatible
// with Chrome Extension MV3 CSP (e.g. @googlemaps/js-api-loader which
// dynamically injects <script> tags).
module.exports = {};
2 changes: 1 addition & 1 deletion src/pages/tasking/components/alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ function buildDefaultRules(vm) {
{
id: 'unacked-notifications',
level: 'danger',
title: 'Unacknowledged notifications',
title: 'Unacknowledged ICEMS notifications',
active: unackedNotifications.length > 0,
items: unackedNotifications.slice(0, 10).map(asItem),
count: unackedNotifications.length,
Expand Down
69 changes: 51 additions & 18 deletions src/pages/tasking/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ import { registerSESUnitLocationsLayer } from "./mapLayers/geoservices.js";
import { registerTransportIncidentsLayer } from "./mapLayers/transport.js";
import { renderFRAOSLayer } from "./mapLayers/frao.js";
import { registerHazardWatchWarningsLayer } from "./mapLayers/hazardwatch.js"
import { registerPowerBoundariesGridLayer } from "./mapLayers/power.js";
import { registerWaterNSWBoundariesLayer, registerEPAContaminationSitesLayer } from "./mapLayers/waternsw.js";
import { registerBOMLandWarningsLayer } from "./mapLayers/bom.js";
import { registerRainRadarLayer } from "./mapLayers/weather.js";

import { fetchHqDetailsSummary } from './utils/hqSummary.js';

Expand Down Expand Up @@ -1245,7 +1249,7 @@ function VM() {

for (const m of s.matchAll(re)) {
const tok = _normAssetName(m[0]); // strips spaces/punct => "par56"
if (!tok || seen.has(tok)) continue;
if (!tok) continue;
seen.add(tok);
tokens.push(tok);
}
Expand Down Expand Up @@ -1291,23 +1295,27 @@ function VM() {
const matchedAssets = new Set();
const usedTokens = new Set();

// 1) EXACT first: token === assetName
// 1) EXACT first: token === assetName (one match per token)
// If multiple exact matches exist, prefer non-Portable resourceType
for (const tok of tokens) {
const exactList = byName.get(tok);
if (!exactList || !exactList.length) continue;

// allow multiple exact matches (duplicate assets with same name)
let addedAny = false;
for (const a of exactList) {
if (matchedAssetIds.has(a.id())) continue;
matchedAssetIds.add(a.id());
matchedAssets.add(a);
addedAny = true;
}
// Filter to unmatched candidates
const candidates = exactList.filter(a => !matchedAssetIds.has(a.id()));
if (!candidates.length) continue;

// Sort so that Portable resourceType comes last (least preferable)
candidates.sort((a, b) => {
const aPortable = (ko.unwrap(a.resourceType) || '').toLowerCase() === 'portable' ? 1 : 0;
const bPortable = (ko.unwrap(b.resourceType) || '').toLowerCase() === 'portable' ? 1 : 0;
return aPortable - bPortable;
});

// consume the token if it produced at least one exact match,
// so it won't be used for fuzzy matching.
if (addedAny) usedTokens.add(tok);
const best = candidates[0];
matchedAssetIds.add(best.id());
matchedAssets.add(best);
usedTokens.add(tok);
}

// 2) FUZZY second (token consumed once; asset matched once)
Expand Down Expand Up @@ -1663,6 +1671,10 @@ function VM() {

// Maintain markers only for currently filtered assets
self.filteredTrackableAssets.subscribe((changes) => {
// bail fast if the layer is not currently visible
if (!self.mapVM || !map.hasLayer(self.mapVM.assetLayer)) {
return;
}
changes.forEach(ch => {
const a = ch.value;
if (ch.status === 'added') {
Expand Down Expand Up @@ -1690,21 +1702,37 @@ function VM() {
});
}, null, "arrayChange");

// --- Matched asset layer: populate / tear-down on toggle ---
map.on('layeradd', (ev) => {
if (ev.layer !== self.mapVM.assetLayer) return;
const assets = self.filteredTrackableAssets?.() || [];
assets.forEach(a => {
attachAssetMarker(ko, map, self, a);
});
});

map.on('layerremove', (ev) => {
if (ev.layer !== self.mapVM.assetLayer) return;
const assets = self.filteredTrackableAssets?.() || [];
assets.forEach(a => {
detachAssetMarker(ko, map, self, a);
});
});

// --- Unmatched asset layer: populate / tear-down on toggle ---
map.on('layeradd', (ev) => {
if (ev.layer !== self.mapVM.unmatchedAssetLayer) return;
// initial populate unmatchedTrackableAssets only when layer becomes visible
const assets = self.unmatchedTrackableAssets?.() || [];
assets.forEach(a => {
attachUnmatchedAssetMarker(ko, self.map, self, a);
attachUnmatchedAssetMarker(ko, map, self, a);
});
});

// catch the unmatchedTrackableAssets layer being turned off to remove markers
map.on('layerremove', (ev) => {
if (ev.layer !== self.mapVM.unmatchedAssetLayer) return;
const assets = self.unmatchedTrackableAssets?.() || [];
assets.forEach(a => {
attachUnmatchedAssetMarker(ko, self.map, self, a);
detachUnmatchedAssetMarker(ko, map, self, a);
});
});

Expand Down Expand Up @@ -2143,6 +2171,11 @@ function VM() {
registerHazardWatchWarningsLayer(self, apiHost);
registerSESUnitLocationsLayer(self);
renderFRAOSLayer(self, map, getToken, apiHost, params);
registerPowerBoundariesGridLayer(self, map);
registerWaterNSWBoundariesLayer(self);
registerEPAContaminationSitesLayer(self);
registerBOMLandWarningsLayer(self);
registerRainRadarLayer(self, map);

// --- Layers Drawer (under zoom)
const LayersDrawer = L.Control.extend({
Expand Down Expand Up @@ -2272,7 +2305,7 @@ function VM() {

const gid = safeId(groupKey);
const storeKey = `layers.ovgrp.${gid}`;
const open = localStorage.getItem(storeKey) !== "0"; // default open
const open = localStorage.getItem(storeKey) === "1"; // default closed

const item = document.createElement("div");
item.className = "accordion-item";
Expand Down
186 changes: 186 additions & 0 deletions src/pages/tasking/mapLayers/bom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import * as esri from "esri-leaflet";

const BASE_URL =
"https://services1.arcgis.com/vkTwD8kHw2woKBqV/arcgis/rest/services/BOM_Land_Warnings_RO/FeatureServer";

/* ── Flood Warning severity colours (layer 0 & 1) ────────────── */
function floodSeverityStyle(severity) {
switch (severity) {
case "MAJ":
return { color: "#E60000", fillColor: "#FF0000", fillOpacity: 0.4 };
case "MOD":
return { color: "#FFAA00", fillColor: "#FFAA00", fillOpacity: 0.4 };
case "MIN":
return { color: "#38A800", fillColor: "#B4D79E", fillOpacity: 0.4 };
case "BMIN":
return { color: "#00C5FF", fillColor: "#FFFFFF", fillOpacity: 0.3 };
case "FINAL":
return { color: "#CDCD66", fillColor: "#F2F2E1", fillOpacity: 0.3 };
case "UNCL":
default:
return { color: "#C8C3FF", fillColor: "#EFEDFF", fillOpacity: 0.3 };
}
}

/* ── Fire Weather severity colours (layer 4) ──────────────────── */
function fireSeverityStyle(severity) {
switch (severity) {
case "CAT":
return { color: "#730000", fillColor: "#730000", fillOpacity: 0.45 };
case "EXT":
return { color: "#E60000", fillColor: "#E60000", fillOpacity: 0.4 };
case "SEV":
default:
return { color: "#E69800", fillColor: "#E69800", fillOpacity: 0.4 };
}
}

/* ── Human-readable severity label ────────────────────────────── */
function severityLabel(code) {
const map = {
MAJ: "Major",
MOD: "Moderate",
MIN: "Minor",
BMIN: "Below Minor",
UNCL: "Generalised",
FINAL: "Final Warning",
SEV: "Severe",
CAT: "Catastrophic",
EXT: "Extreme",
};
return map[code] || code || "";
}

/**
* Register a single toggle that draws all five BOM Land Warning sub-layers.
*/
export function registerBOMLandWarningsLayer(vm) {
vm.mapVM.registerPollingLayer("bomLandWarnings", {
label: "BOM Land Warnings",
menuGroup: "Bureau of Meteorology",
refreshMs: 300000, // 5 min – warnings update frequently
visibleByDefault: localStorage.getItem(`ov.bomLandWarnings`) || false,
fetchFn: async () => {
return {};
},
drawFn: (layerGroup, data) => {
if (!data) return;

/* --- 0 Flood Warning ----------------------------------- */
const floodWarning = esri.featureLayer({
url: `${BASE_URL}/0`,
pane: "pane-lowest",
where: "1=1",
fields: ["OBJECTID", "name", "severity", "status", "phase", "start_time_local", "end_time_local"],
style: (feature) => {
const s = floodSeverityStyle(feature.properties.severity);
return { weight: 2, color: s.color, opacity: 1, fill: true, fillColor: s.fillColor, fillOpacity: s.fillOpacity };
},
});
floodWarning.bindPopup((layer) => {
const p = layer.feature.properties;
return `<strong>🌊 Flood Warning</strong><br>
${p.name || "Unknown"}<br>
<strong>Severity:</strong> ${severityLabel(p.severity)}<br>
${p.phase ? `<strong>Phase:</strong> ${p.phase}<br>` : ""}
${p.start_time_local ? `<strong>From:</strong> ${new Date(p.start_time_local).toLocaleString()}<br>` : ""}
${p.end_time_local ? `<strong>Until:</strong> ${new Date(p.end_time_local).toLocaleString()}` : ""}`;
});
layerGroup.addLayer(floodWarning);

/* --- 1 Flood Watch -------------------------------------- */
const floodWatch = esri.featureLayer({
url: `${BASE_URL}/1`,
pane: "pane-lowest",
where: "1=1",
fields: ["OBJECTID", "name", "severity", "status", "phase", "start_time_local", "end_time_local"],
style: (feature) => {
const s = floodSeverityStyle(feature.properties.severity);
return { weight: 2, dashArray: "6 4", color: s.color, opacity: 1, fill: true, fillColor: s.fillColor, fillOpacity: s.fillOpacity * 0.6 };
},
});
floodWatch.bindPopup((layer) => {
const p = layer.feature.properties;
return `<strong>👁️ Flood Watch</strong><br>
${p.name || "Unknown"}<br>
<strong>Severity:</strong> ${severityLabel(p.severity)}<br>
${p.phase ? `<strong>Phase:</strong> ${p.phase}<br>` : ""}
${p.start_time_local ? `<strong>From:</strong> ${new Date(p.start_time_local).toLocaleString()}<br>` : ""}
${p.end_time_local ? `<strong>Until:</strong> ${new Date(p.end_time_local).toLocaleString()}` : ""}`;
});
layerGroup.addLayer(floodWatch);

/* --- 2 Severe Weather Warning --------------------------- */
const severeWeather = esri.featureLayer({
url: `${BASE_URL}/2`,
pane: "pane-lowest",
where: "1=1",
fields: ["OBJECTID", "product", "phenomena", "warning", "validfrom_utc", "validto_utc"],
style: () => ({
weight: 1.5,
color: "#6E6E6E",
opacity: 1,
fill: true,
fillColor: "#F7E363",
fillOpacity: 0.45,
}),
});
severeWeather.bindPopup((layer) => {
const p = layer.feature.properties;
return `<strong>⚠️ Severe Weather Warning</strong><br>
${p.phenomena || p.product || "Unknown"}<br>
${p.warning ? `${p.warning}<br>` : ""}
${p.validfrom_utc ? `<strong>From:</strong> ${new Date(p.validfrom_utc).toLocaleString()}<br>` : ""}
${p.validto_utc ? `<strong>Until:</strong> ${new Date(p.validto_utc).toLocaleString()}` : ""}`;
});
layerGroup.addLayer(severeWeather);

/* --- 3 Thunderstorm Warning ----------------------------- */
const thunderstorm = esri.featureLayer({
url: `${BASE_URL}/3`,
pane: "pane-lowest",
where: "1=1",
fields: ["OBJECTID", "severity", "status", "phase", "start_time_local", "end_time_local", "state_code"],
style: () => ({
weight: 1.5,
color: "#6E6E6E",
opacity: 1,
fill: true,
fillColor: "#C896FF",
fillOpacity: 0.4,
}),
});
thunderstorm.bindPopup((layer) => {
const p = layer.feature.properties;
return `<strong>⛈️ Thunderstorm Warning</strong><br>
<strong>Severity:</strong> ${severityLabel(p.severity)}<br>
${p.phase ? `<strong>Phase:</strong> ${p.phase}<br>` : ""}
${p.start_time_local ? `<strong>From:</strong> ${new Date(p.start_time_local).toLocaleString()}<br>` : ""}
${p.end_time_local ? `<strong>Until:</strong> ${new Date(p.end_time_local).toLocaleString()}` : ""}`;
});
layerGroup.addLayer(thunderstorm);

/* --- 4 Fire Weather Warnings ---------------------------- */
const fireWeather = esri.featureLayer({
url: `${BASE_URL}/4`,
pane: "pane-lowest",
where: "1=1",
fields: ["OBJECTID", "severity", "status", "headline", "day", "start_time_local", "end_time_local", "state_code"],
style: (feature) => {
const s = fireSeverityStyle(feature.properties.severity);
return { weight: 1.5, color: s.color, opacity: 1, fill: true, fillColor: s.fillColor, fillOpacity: s.fillOpacity };
},
});
fireWeather.bindPopup((layer) => {
const p = layer.feature.properties;
return `<strong>🔥 Fire Weather Warning</strong><br>
${p.headline || ""}<br>
<strong>Severity:</strong> ${severityLabel(p.severity)}<br>
${p.day ? `<strong>Day:</strong> ${p.day}<br>` : ""}
${p.start_time_local ? `<strong>From:</strong> ${new Date(p.start_time_local).toLocaleString()}<br>` : ""}
${p.end_time_local ? `<strong>Until:</strong> ${new Date(p.end_time_local).toLocaleString()}` : ""}`;
});
layerGroup.addLayer(fireWeather);
},
});
}
15 changes: 3 additions & 12 deletions src/pages/tasking/mapLayers/geoservices.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BeaconClient from "../../../shared/BeaconClient";
import { unitColorMap } from "../utils/unitColorMap";
import L from "leaflet";


Expand Down Expand Up @@ -152,7 +153,7 @@ export function registerSESUnitsZonesHybridGridLayer(vm, map) {
drawFn: (layerGroup, data) => {

if (!data) return;
const LABEL_ZOOM_THRESHOLD = 10; // tweak to taste
const LABEL_ZOOM_THRESHOLD = 9; // tweak to taste

const vectorGrid = L.vectorGrid.protobuf(
`https://map.lighthouse-extension.com/sesunits/tiles/{z}/{x}/{y}.pbf`, // Replace with actual path
Expand Down Expand Up @@ -266,16 +267,7 @@ function zoneFillColor(code) {

function colorByUnitCode(code) {
if (!code) return '#999';

let hash = 0;
for (let i = 0; i < code.length; i++) {
hash = (hash << 5) - hash + code.charCodeAt(i);
hash |= 0;
}

// Spread around 360° hue wheel
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 50%)`;
return unitColorMap[code] || '#999';
}


Expand Down Expand Up @@ -311,7 +303,6 @@ export function registerSESUnitLocationsLayer(vm) {
marker.on('popupopen', async () => {
try {
const details = await vm.fetchHQDetails(feature.properties.HQNAME);
console.log("Fetched HQ details for popup:", details);
const popupContent = `
<div style="background-color: #333; color: #fff; padding: 5px; text-align: center; font-weight: bold; width: 300px;">
${name}
Expand Down
Loading