diff --git a/frontend/src/config/index.ts b/frontend/src/config/index.ts
index 7421efd1..0f781af5 100644
--- a/frontend/src/config/index.ts
+++ b/frontend/src/config/index.ts
@@ -524,9 +524,16 @@ export const OFFSET_STEP: number = parseIntEnv(ENVS.OFFSET_STEP, 0.5);
export const ELEMENT_DISTANCE_FROM_NAVBAR: number = 10;
/**
- * Mapswipe agreement colors for fill and outline.
+ * Mapswipe agreement colors for fill and outline.
* Green for agreement is 1, Red for agreement is 0, and Purple for 0 < agreement < 1.
*/
-export const MAPSWIPE_AGREEMENT_FILL_COLORS = { green: "#22c55e", red: "#ef4444", purple: "#663399" };
-export const MAPSWIPE_AGREEMENT_OUTLINE_COLORS = { green: "#16a34a", red: "#dc2626", purple: "#4b2270" };
-
+export const MAPSWIPE_AGREEMENT_FILL_COLORS = {
+ green: PREDICTED_LAYER_STATUS_COLORS[PredictedFeatureStatus.ACCEPTED],
+ red: PREDICTED_LAYER_STATUS_COLORS[PredictedFeatureStatus.REJECTED],
+ purple: PREDICTED_LAYER_STATUS_COLORS[PredictedFeatureStatus.UNTOUCHED],
+};
+export const MAPSWIPE_AGREEMENT_OUTLINE_COLORS = {
+ green: PREDICTED_LAYER_STATUS_COLORS[PredictedFeatureStatus.ACCEPTED],
+ red: PREDICTED_LAYER_STATUS_COLORS[PredictedFeatureStatus.REJECTED],
+ purple: PREDICTED_LAYER_STATUS_COLORS[PredictedFeatureStatus.UNTOUCHED],
+};
diff --git a/frontend/src/constants/ui-contents/map-content.ts b/frontend/src/constants/ui-contents/map-content.ts
index e5f4923d..605494b1 100644
--- a/frontend/src/constants/ui-contents/map-content.ts
+++ b/frontend/src/constants/ui-contents/map-content.ts
@@ -6,4 +6,9 @@ export const MAP_CONTENT: TMapContent = {
tooltip: "Click to adjust the map view to fit the bounds.",
},
},
+ agreementLegend: {
+ fullAgreement: "Full Agreement (1)",
+ partialAgreement: "Partial Agreement (0–1)",
+ noAgreement: "No Agreement (0)",
+ },
};
diff --git a/frontend/src/enums/mapswipe.ts b/frontend/src/enums/mapswipe.ts
index b1b69f62..55a550a7 100644
--- a/frontend/src/enums/mapswipe.ts
+++ b/frontend/src/enums/mapswipe.ts
@@ -16,3 +16,9 @@ export enum MapSwipeProcessingStatus {
READY_TO_PUBLISH = "READY_TO_PUBLISH",
DRAFT = "DRAFT",
}
+
+export enum AgreementStatus {
+ FULL = "full",
+ PARTIAL = "partial",
+ NONE = "none",
+}
diff --git a/frontend/src/features/mapswipe/components/agreement-legend.tsx b/frontend/src/features/mapswipe/components/agreement-legend.tsx
new file mode 100644
index 00000000..c3243e09
--- /dev/null
+++ b/frontend/src/features/mapswipe/components/agreement-legend.tsx
@@ -0,0 +1,30 @@
+import { Legend } from "@/features/start-mapping/components";
+import { AgreementStatus } from "@/enums/mapswipe";
+import { MAP_CONTENT } from "@/constants";
+import { LegendItem } from "@/features/start-mapping/components/map/legend-control";
+import { MAPSWIPE_AGREEMENT_FILL_COLORS } from "@/config";
+
+const agreementLegendItems: (LegendItem & { status: AgreementStatus })[] = [
+ {
+ status: AgreementStatus.FULL,
+ label: MAP_CONTENT.agreementLegend.fullAgreement,
+ fillColor: MAPSWIPE_AGREEMENT_FILL_COLORS.green,
+ fillOpacity: 0.3,
+ },
+ {
+ status: AgreementStatus.PARTIAL,
+ label: MAP_CONTENT.agreementLegend.partialAgreement,
+ fillColor: MAPSWIPE_AGREEMENT_FILL_COLORS.purple,
+ fillOpacity: 0.3,
+ },
+ {
+ status: AgreementStatus.NONE,
+ label: MAP_CONTENT.agreementLegend.noAgreement,
+ fillColor: MAPSWIPE_AGREEMENT_FILL_COLORS.red,
+ fillOpacity: 0.3,
+ },
+];
+
+export const AgreementLegend = () => {
+ return ;
+};
diff --git a/frontend/src/features/models/components/maps/training-area-map.tsx b/frontend/src/features/models/components/maps/training-area-map.tsx
index 40df1da1..c9217558 100644
--- a/frontend/src/features/models/components/maps/training-area-map.tsx
+++ b/frontend/src/features/models/components/maps/training-area-map.tsx
@@ -9,10 +9,11 @@ import {
MapLayerMouseEvent,
Popup,
SourceSpecification,
- ExpressionSpecification
+ ExpressionSpecification,
} from "maplibre-gl";
+import { AgreementLegend } from "@/features/mapswipe/components/agreement-legend";
-import { showErrorToast, addLayers, addSources} from "@/utils";
+import { showErrorToast, addLayers, addSources } from "@/utils";
import {
MAPSWIPE_AGREEMENT_FILL_COLORS,
MAPSWIPE_AGREEMENT_OUTLINE_COLORS,
@@ -28,16 +29,20 @@ import {
TRAINING_AREAS_AOI_OUTLINE_WIDTH,
} from "@/config";
+type VectorLayerMeta = LayerSpecification & {
+ fields?: Record;
+};
+
type Metadata = {
name?: string;
type?: string;
tilestats?: unknown;
- vector_layers: LayerSpecification[];
+ vector_layers: VectorLayerMeta[];
};
// Choropleth fill color for MapSwipe results based on "agreement" property:
// agreement === 1 = green
// agreement === 0 = red
-// 0 < agreement < 1 = purple (rebeccapurple #663399)
+// 0 < agreement < 1 = purple
const buildAgreementColorExpression = (
defaultColor: string,
colors: { green: string; red: string; purple: string },
@@ -52,7 +57,6 @@ const buildAgreementColorExpression = (
colors.purple,
];
-
const getLayerConfigs = (
layerType: string,
isPredictionResult: boolean = false,
@@ -69,7 +73,10 @@ const getLayerConfigs = (
return {
fill: {
"fill-color": isPredictionResult
- ? buildAgreementColorExpression(defaultFillColor, MAPSWIPE_AGREEMENT_FILL_COLORS)
+ ? buildAgreementColorExpression(
+ defaultFillColor,
+ MAPSWIPE_AGREEMENT_FILL_COLORS,
+ )
: defaultFillColor,
"fill-opacity": isPredictionResult
? 0.6
@@ -79,7 +86,10 @@ const getLayerConfigs = (
},
outline: {
"line-color": isPredictionResult
- ? buildAgreementColorExpression(defaultOutlineColor, MAPSWIPE_AGREEMENT_OUTLINE_COLORS)
+ ? buildAgreementColorExpression(
+ defaultOutlineColor,
+ MAPSWIPE_AGREEMENT_OUTLINE_COLORS,
+ )
: defaultOutlineColor,
"line-width": isAoi
? TRAINING_AREAS_AOI_OUTLINE_WIDTH
@@ -122,6 +132,7 @@ export const TrainingAreaMap = ({
const { mapContainerRef, map } = useMapInstance(true);
const [vectorLayers, setVectorLayers] = useState([]);
+ const [hasAgreement, setHasAgreement] = useState(false);
const popupRef = useRef(null);
@@ -141,7 +152,10 @@ export const TrainingAreaMap = ({
const mapLayers: LayerSpecification[] = useMemo(
() =>
vectorLayers.flatMap((layer) => {
- const { fill, outline, circle } = getLayerConfigs(layer.id, isPredictionResult);
+ const { fill, outline, circle } = getLayerConfigs(
+ layer.id,
+ isPredictionResult,
+ );
const layers: LayerSpecification[] = [
{
@@ -302,7 +316,11 @@ export const TrainingAreaMap = ({
const metadata = (await pmtilesFile.getMetadata()) as Metadata;
const layers = metadata.vector_layers;
-
+ if (isPredictionResult) {
+ setHasAgreement(
+ layers.some((layer) => layer.fields && "agreement" in layer.fields),
+ );
+ }
setVectorLayers(layers);
} catch (error) {
console.error("Error loading PMTiles:", error);
@@ -362,6 +380,8 @@ export const TrainingAreaMap = ({
mapContainerRef={mapContainerRef}
map={map}
showCurrentZoom
- />
+ >
+ {isPredictionResult && hasAgreement && }
+
);
};
diff --git a/frontend/src/features/start-mapping/components/map/legend-control.tsx b/frontend/src/features/start-mapping/components/map/legend-control.tsx
index 4689cb74..9817a86e 100644
--- a/frontend/src/features/start-mapping/components/map/legend-control.tsx
+++ b/frontend/src/features/start-mapping/components/map/legend-control.tsx
@@ -6,6 +6,12 @@ import { useState, useCallback } from "react";
import { PredictedFeatureStatus } from "@/enums/start-mapping";
import { PREDICTED_LAYER_STATUS_COLORS } from "@/config";
+export type LegendItem = {
+ label: string;
+ fillColor: string;
+ fillOpacity: number;
+};
+
const statusLegend = [
{
status: PredictedFeatureStatus.ACCEPTED,
@@ -52,9 +58,11 @@ const FillLegendStyle = ({
export const Legend = ({
disableDefaultPrediction = false,
title = "Predictions",
+ items,
}: {
disableDefaultPrediction?: boolean;
title?: string;
+ items?: LegendItem[];
}) => {
const { isSmallViewport } = useScreenSize();
const [expandLegend, setExpandLegend] = useState(true);
@@ -63,6 +71,14 @@ export const Legend = ({
setExpandLegend((prev) => !prev);
}, []);
+ const legendItems: LegendItem[] = items
+ ? items
+ : statusLegend.filter((v) =>
+ disableDefaultPrediction
+ ? v.status !== PredictedFeatureStatus.UNTOUCHED
+ : v,
+ );
+
return (