diff --git a/components/AlertsDashboard.vue b/components/AlertsDashboard.vue index cbf7365e..55bcd981 100644 --- a/components/AlertsDashboard.vue +++ b/components/AlertsDashboard.vue @@ -103,7 +103,7 @@ const props = defineProps<{ mapboxZoom: number; mapbox3d: boolean; mapbox3dTerrainExaggeration?: number | null | undefined; - mapeoData: FeatureCollection | null; + secondaryData: FeatureCollection | null; primaryDataset: string; secondaryDataset?: string | null; mediaBasePath: string | undefined; @@ -129,7 +129,7 @@ const showSlider = ref(false); const route = useRoute(); const router = useRouter(); -const isMapeo = ref(false); +const isSecondary = ref(false); const selectedFeatureLoading = ref(false); const { fetchRecord } = useRecordCache(); @@ -197,11 +197,11 @@ const { localAlertsData, showSidebar, showIntroPanel, - isMapeo, + isSecondary, ); // Fetch full record on demand when a minimal feature is selected. -// Handles both Mapeo features and alert features — the minimal +// Handles both secondary features and alert features — the minimal // FeatureCollection only carries IDs and map-rendering fields, so // the full record is loaded via the single-record endpoint. let skipNextWatch = false; @@ -214,15 +214,15 @@ watch( } if (!feature) return; - const isMapeoFeature = isMapeo.value && props.secondaryDataset; - const isMinimalAlert = !isMapeo.value && feature.alertID && feature._id; + const isSecondaryFeature = isSecondary.value && props.secondaryDataset; + const isMinimalAlert = !isSecondary.value && feature.alertID && feature._id; - if (!isMapeoFeature && !isMinimalAlert) return; + if (!isSecondaryFeature && !isMinimalAlert) return; const recordId = feature._id || feature.id; if (!recordId) return; - const fetchTable = isMapeoFeature + const fetchTable = isSecondaryFeature ? props.secondaryDataset! : props.primaryDataset; const minimalFeature = { ...feature }; @@ -233,7 +233,7 @@ watch( skipNextWatch = true; let displayRecord: Record; - if (isMapeoFeature) { + if (isSecondaryFeature) { displayRecord = fullRecord ? transformSurveyEntry(fullRecord) : minimalFeature; @@ -328,26 +328,26 @@ const selectInitialAlertFeature = (alertId: string) => { ); map.value.flyTo({ center: [lng, lat], zoom: 15 }); } - isMapeo.value = false; + isSecondary.value = false; } }; /** - * Selects and zooms to a Mapeo feature based on its document ID. - * Mapeo data arrives as a GeoJSON FeatureCollection with normalized numeric IDs + * Selects and zooms to a secondary feature based on its document ID. + * Secondary data arrives as a GeoJSON FeatureCollection with normalized numeric IDs * (via MurmurHash) for Mapbox feature-state compatibility. */ -const selectInitialMapeoFeature = (mapeoDocId: string) => { - const feature = props.mapeoData?.features.find( - (mapeoFeature) => - mapeoFeature.properties?._id === mapeoDocId || - mapeoFeature.properties?.id === mapeoDocId, +const selectInitialSecondaryFeature = (secondaryDocId: string) => { + const feature = props.secondaryData?.features.find( + (secondaryFeature) => + secondaryFeature.properties?._id === secondaryDocId || + secondaryFeature.properties?.id === secondaryDocId, ); if (!feature) return; - selectFeature(feature, "mapeo-data"); - isMapeo.value = true; + selectFeature(feature, "secondary-data"); + isSecondary.value = true; // Zoom to the feature if (feature.geometry.type === "Point") { const [lng, lat] = (feature.geometry as GeoJSON.Point).coordinates; @@ -430,14 +430,24 @@ onMounted(() => { // Only show slider if there are date options showSlider.value = dateOptions.value && dateOptions.value.length > 0; - // Check for alertId or mapeoDocId in URL and select the corresponding feature + // Check for alertId or secondaryDocId in URL and select the corresponding feature const alertId = route.query.alertId as string; - const mapeoDocId = route.query.mapeoDocId as string; + const legacyMapeoDocId = route.query.mapeoDocId as string; + const secondaryDocId = (route.query.secondaryDocId || + legacyMapeoDocId) as string; + + // Silently migrate the legacy mapeoDocId param to secondaryDocId + if (legacyMapeoDocId && !route.query.secondaryDocId) { + const query = { ...route.query }; + delete query.mapeoDocId; + query.secondaryDocId = legacyMapeoDocId; + router.replace({ query }); + } if (alertId) { selectInitialAlertFeature(alertId); - } else if (mapeoDocId && props.mapeoData) { - selectInitialMapeoFeature(mapeoDocId); + } else if (secondaryDocId && props.secondaryData) { + selectInitialSecondaryFeature(secondaryDocId); } // Load incidents on dashboard initialization @@ -469,8 +479,8 @@ const emit = defineEmits(["reset-legend-visibility"]); const featuresUnderCursor = ref(0); const hasLineStrings = ref(false); const hasPoints = ref(false); -const mapeoDataColor = ref(); -const MAPEO_INTERACTIVE_LAYER_IDS = ["mapeo-data"]; +const secondaryDataColor = ref(); +const SECONDARY_INTERACTIVE_LAYER_IDS = ["secondary-data"]; const additionalSelectableLayerIds = computed(() => (props.mapLegendLayerIds || "") .split(",") @@ -1151,31 +1161,31 @@ const addAlertsData = async () => { }; /** - * Adds (optional) Mapeo data to the map as a GeoJSON FeatureCollection source + * Adds (optional) secondary data to the map as a GeoJSON FeatureCollection source * with associated circle and symbol layers. */ -const addMapeoData = () => { - if (!props.mapeoData || props.mapeoData.features.length === 0) { +const addSecondaryData = () => { + if (!props.secondaryData || props.secondaryData.features.length === 0) { return; } - mapeoDataColor.value = - props.mapeoData.features[0]?.properties?.["filter-color"]; + secondaryDataColor.value = + props.secondaryData.features[0]?.properties?.["filter-color"]; // Add the source to the map - if (!map.value.getSource("mapeo-data")) { - map.value.addSource("mapeo-data", { + if (!map.value.getSource("secondary-data")) { + map.value.addSource("secondary-data", { type: "geojson", - data: props.mapeoData, + data: props.secondaryData, }); } // Add a layer for Point features - if (!map.value.getLayer("mapeo-data")) { + if (!map.value.getLayer("secondary-data")) { map.value.addLayer({ - id: "mapeo-data", + id: "secondary-data", type: "circle", - source: "mapeo-data", + source: "secondary-data", filter: ["==", "$type", "Point"], paint: { "circle-radius": 6, @@ -1203,7 +1213,7 @@ const addMapeoData = () => { } // Add event listeners - const interactiveLayers = MAPEO_INTERACTIVE_LAYER_IDS.filter((layerId) => + const interactiveLayers = SECONDARY_INTERACTIVE_LAYER_IDS.filter((layerId) => map.value.getLayer(layerId), ); @@ -1250,7 +1260,7 @@ const addMapeoData = () => { }); }; /** - * Prepares the map canvas content by adding alert and Mapeo data, + * Prepares the map canvas content by adding alert and secondary data, * and the map legend. */ const prepareMapCanvasContent = async () => { @@ -1258,8 +1268,8 @@ const prepareMapCanvasContent = async () => { if (props.alertsData) { promises.push(addAlertsData()); } - if (props.mapeoData) { - promises.push(addMapeoData()); + if (props.secondaryData) { + promises.push(addSecondaryData()); } await Promise.all(promises); prepareMapLegendContent(); @@ -1286,7 +1296,7 @@ const handleBufferClick = (e: MapMouseEvent) => { "previous-alerts-polygon", "most-recent-alerts-centroids", "previous-alerts-centroids", - ...MAPEO_INTERACTIVE_LAYER_IDS, + ...SECONDARY_INTERACTIVE_LAYER_IDS, ].filter((layerId) => map.value.getLayer(layerId)); if (directHitLayers.length > 0) { @@ -1371,13 +1381,17 @@ const prepareMapLegendContent = () => { map.value.once("idle", () => { const legendItems: MapLegendItem[] = []; - // Add mapeo-data layer first to ensure it's always on top - if (props.mapeoData) { + // Add secondary-data layer first to ensure it's always on top + if (props.secondaryData) { legendItems.push({ - id: "mapeo-data", - name: "Mapeo data", + id: "secondary-data", + name: props.secondaryDataset + ? props.secondaryDataset + .replace(/_/g, " ") + .replace(/^\w/, (character) => character.toUpperCase()) + : t("secondaryData"), type: "circle", - color: mapeoDataColor.value || "#000000", + color: secondaryDataColor.value || "#000000", visible: true, }); } @@ -1409,7 +1423,7 @@ const prepareMapLegendContent = () => { const additionalLayers = prepareMapLegendLayers( map.value, props.mapLegendLayerIds, - mapeoDataColor.value, + secondaryDataColor.value, ); if (additionalLayers) { legendItems.push(...(additionalLayers as MapLegendItem[])); @@ -1464,8 +1478,8 @@ const toggleLayerVisibility = (item: MapLegendItem) => { } } }); - } else if (item.id === "mapeo-data") { - MAPEO_INTERACTIVE_LAYER_IDS.forEach((layerId) => { + } else if (item.id === "secondary-data") { + SECONDARY_INTERACTIVE_LAYER_IDS.forEach((layerId) => { if (map.value.getLayer(layerId)) { map.value.setLayoutProperty(layerId, "visibility", visibility); } @@ -1475,7 +1489,7 @@ const toggleLayerVisibility = (item: MapLegendItem) => { } }); } else { - // Handle individual layers (mapeo-data, etc.) + // Handle individual layers (secondary-data, etc.) utilsToggleLayerVisibility(map.value, item); } }; @@ -1681,8 +1695,8 @@ const resetToInitialState = () => { } } }); - } else if (item.id === "mapeo-data") { - MAPEO_INTERACTIVE_LAYER_IDS.forEach((layerId) => { + } else if (item.id === "secondary-data") { + SECONDARY_INTERACTIVE_LAYER_IDS.forEach((layerId) => { if (map.value.getLayer(layerId)) { map.value.setLayoutProperty(layerId, "visibility", visibility); } @@ -1696,7 +1710,7 @@ const resetToInitialState = () => { } }); } else { - // Handle individual layers (mapeo-data, etc.) + // Handle individual layers (secondary-data, etc.) utilsToggleLayerVisibility(map.value, item); } }); @@ -1729,7 +1743,7 @@ onBeforeUnmount(() => { :calculate-hectares="calculateHectares" :date-options="dateOptions" :export-table-name=" - isMapeo ? secondaryDataset || primaryDataset : primaryDataset + isSecondary ? secondaryDataset || primaryDataset : primaryDataset " :feature="selectedFeature" :feature-loading="selectedFeatureLoading" @@ -1737,7 +1751,7 @@ onBeforeUnmount(() => { :file-paths="imageUrl" :geojson-selection="filteredData" :is-alert="isAlert" - :is-mapeo="isMapeo" + :is-secondary="isSecondary" :is-alerts-dashboard="true" :local-alerts-data="localAlertsData" :logo-url="logoUrl" diff --git a/components/alerts/IncidentsSidebar.vue b/components/alerts/IncidentsSidebar.vue index fa9ccdc9..77473d07 100644 --- a/components/alerts/IncidentsSidebar.vue +++ b/components/alerts/IncidentsSidebar.vue @@ -13,6 +13,7 @@ import { buildIncidentMetadataCsv, triggerTextDownload, } from "@/utils/incidentHelpers"; +import { isSecondaryFeatureType } from "@/types"; import type { AnnotatedCollection, CollectionEntry, @@ -127,7 +128,11 @@ const downloadIncidentFeatures = () => { }; const showCreateForm = ref(false); -const { showCopied, copyLink } = useCopyLink(["alertId", "mapeoDocId"]); +const { showCopied, copyLink } = useCopyLink([ + "alertId", + "mapeoDocId", + "secondaryDocId", +]); const formData = ref({ name: "", description: "", @@ -175,12 +180,12 @@ const isLoadingMore = computed(() => props.isLoadingMore === true); const selectedSourceSummary = computed(() => { const summary = { alerts: 0, - mapeoData: 0, + secondary: 0, }; props.selectedSources.forEach((source) => { - if (source.feature_type === "mapeo") { - summary.mapeoData += 1; + if (isSecondaryFeatureType(source.feature_type)) { + summary.secondary += 1; } else { summary.alerts += 1; } @@ -443,8 +448,8 @@ const handleClose = () => {

{{ - $t("incidents.selectedMapeoCount", { - count: selectedSourceSummary.mapeoData, + $t("incidents.selectedSecondaryCount", { + count: selectedSourceSummary.secondary, }) }}

diff --git a/components/config/ConfigAlerts.vue b/components/config/ConfigAlerts.vue index 37c0518a..8a0d1273 100644 --- a/components/config/ConfigAlerts.vue +++ b/components/config/ConfigAlerts.vue @@ -20,9 +20,14 @@ const emit = defineEmits<{ type Tag = { text: string }; +const resolveCategoryIds = (config: ViewConfig): string | undefined => + config.SECONDARY_CATEGORY_IDS || config.MAPEO_CATEGORY_IDS; + +const initialCategoryIds = resolveCategoryIds(props.config); + const initialTags: Record = { - MAPEO_CATEGORY_IDS: props.config.MAPEO_CATEGORY_IDS - ? props.config.MAPEO_CATEGORY_IDS.split(",").map((tag) => ({ text: tag })) + SECONDARY_CATEGORY_IDS: initialCategoryIds + ? initialCategoryIds.split(",").map((tag) => ({ text: tag })) : [], }; @@ -34,7 +39,10 @@ const { tags, handleTagsChanged: rawHandleTagsChanged } = updateTags( const handleTagsChanged = (key: string, newTags: Tag[]): void => { rawHandleTagsChanged(key, newTags); const values = newTags.map((tag) => tag.text).join(","); - emit("updateConfig", { [key]: values }); + emit("updateConfig", { + SECONDARY_CATEGORY_IDS: values, + MAPEO_CATEGORY_IDS: undefined, + }); }; @@ -47,7 +55,7 @@ const handleTagsChanged = (key: string, newTags: Tag[]): void => { > {{ $t(toCamelCase(key)) }} -