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 (