Skip to content

Commit

Permalink
feat(frontend): render points on both frontends, with colour determin…
Browse files Browse the repository at this point in the history
…ed by status (#2174)

* fix: display points on project details (map style to handle points)

* feat(mapper): display points to map with pin colour determined by status

* fix(mapper): separate point and polygon entity map rendering

* refactor(backend): do not upload new_geom_type field during project creation

* docs: update comment on project_crud for clarity
  • Loading branch information
spwoodcock authored Feb 12, 2025
1 parent debd7c2 commit ceb31a3
Show file tree
Hide file tree
Showing 21 changed files with 152 additions and 106 deletions.
4 changes: 2 additions & 2 deletions src/backend/app/projects/project_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ async def get_or_set_data_extract_url(
str: URL to fgb file in S3.
"""
project_id = db_project.id
# If url, get extract
# If not url, get new extract / set in db
# If url passed, get extract
# If no url passed, get new extract / set in db
if not url:
existing_url = db_project.data_extract_url

Expand Down
4 changes: 3 additions & 1 deletion src/backend/app/projects/project_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from app.central.central_schemas import ODKCentralDecrypted, ODKCentralIn
from app.config import decrypt_value, encrypt_value, settings
from app.db.enums import BackgroundTaskStatus, GeomStatus, ProjectPriority
from app.db.enums import BackgroundTaskStatus, DbGeomType, GeomStatus, ProjectPriority
from app.db.models import DbBackgroundTask, DbBasemap, DbProject, slugify
from app.db.postgis_utils import (
geojson_to_featcol,
Expand Down Expand Up @@ -182,6 +182,8 @@ class ProjectIn(ProjectInBase, ODKCentralIn):
xform_category: str
# Ensure geojson_pydantic.Polygon
outline: Polygon
# Omit new_geom_type as we calculate this automatically
new_geom_type: Annotated[Optional[DbGeomType], Field(exclude=True)] = None

@model_validator(mode="after")
def generate_location_str(self) -> Self:
Expand Down
Binary file added src/frontend/src/assets/images/map-pin-grey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/frontend/src/assets/images/red_marker.png
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -437,32 +437,4 @@ const VectorLayer = ({
return null;
};

// TODO replace with typescript
// VectorLayer.propTypes = {
// // Ensure either geojson or fgbUrl is provided
// geojson: (props, propName, componentName) => {
// if (!props.geojson && !props.fgbUrl) {
// return new Error(`One of 'geojson' or 'fgbUrl' is required in '${componentName}'`);
// }
// if (props.geojson && props.fgbUrl) {
// return new Error(`Only one of 'geojson' or 'fgbUrl' should be provided in '${componentName}'`);
// }
// },
// fgbUrl: (props, propName, componentName) => {
// if (!props.geojson && !props.fgbUrl) {
// return new Error(`One of 'geojson' or 'fgbUrl' is required in '${componentName}'`);
// }
// if (props.geojson && props.fgbUrl) {
// return new Error(`Only one of 'geojson' or 'fgbUrl' should be provided in '${componentName}'`);
// }
// },
// fgbExtent: PropTypes.object,
// style: PropTypes.object,
// zIndex: PropTypes.number,
// zoomToLayer: PropTypes.bool,
// viewProperties: PropTypes.object,
// mapOnClick: PropTypes.func,
// onModify: PropTypes.func,
// };

export default VectorLayer;
4 changes: 0 additions & 4 deletions src/frontend/src/components/createnewproject/SplitTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,6 @@ const SplitTasks = ({ flag, setGeojsonFile, customDataExtractUpload, additionalF
// Create a file object from the Blob
const taskAreaGeojsonFile = new File([taskAreaBlob], 'data.json', { type: 'application/json' });

// FIXME for now hardcoded as Polygon projects (add project creation UI for user selection)
// projectData = { ...projectData, new_geom_type: projectDetails.new_geom_type };
projectData = { ...projectData, new_geom_type: 'POLYGON' };

dispatch(
CreateProjectService(
`${import.meta.env.VITE_API_URL}/projects?org_id=${projectDetails.organisation_id}`,
Expand Down
13 changes: 8 additions & 5 deletions src/frontend/src/components/home/ProjectListMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@ import { ClusterLayer } from '@/components/MapComponent/OpenLayersComponent/Laye
import CoreModules from '@/shared/CoreModules';
import { geojsonObjectModel, geojsonObjectModelType } from '@/constants/geojsonObjectModal';
import { defaultStyles } from '@/components/MapComponent/OpenLayersComponent/helpers/styleUtils';
import MarkerIcon from '@/assets/images/red_marker.png';
import MarkerIcon from '@/assets/images/map-pin-primary.png';
import { useNavigate } from 'react-router-dom';
import { Style, Text, Icon, Fill } from 'ol/style';
import { projectType } from '@/models/home/homeModel';
import LayerSwitchMenu from '../MapComponent/OpenLayersComponent/LayerSwitcher/LayerSwitchMenu';

const getIndividualStyle = (featureProperty) => {
const getIndividualClusterPointStyle = (featureProperty) => {
const style = new Style({
image: new Icon({
anchor: [0.5, 1],
scale: 1.1,
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: MarkerIcon,
scale: 0.08,
}),
text: new Text({
text: featureProperty?.project_id,
fill: new Fill({
color: 'black',
}),
offsetY: 35,
offsetY: 42,
font: '20px Times New Roman',
}),
});
Expand Down Expand Up @@ -93,7 +96,7 @@ const ProjectListMap = () => {
opacity: 90,
}}
mapOnClick={projectClickOnMap}
getIndividualStyle={getIndividualStyle}
getIndividualStyle={getIndividualClusterPointStyle}
/>
)}
</MapComponent>
Expand Down
1 change: 0 additions & 1 deletion src/frontend/src/models/project/projectModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export type projectInfoType = {
bbox: [number, number, number, number];
last_active: string;
num_contributors: number | null;
new_geom_type: NewGeomTypes;
};

export type taskType = {
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/shared/AssetModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ import {
} from '@mui/icons-material';
import LockPng from '@/assets/images/lock.png';
import RedLockPng from '@/assets/images/red-lock.png';
import MapPinRed from '@/assets/images/map-pin-primary.png';
import MapPinGrey from '@/assets/images/map-pin-grey.png';
import { styled, alpha } from '@mui/material/styles';
export default {
ExitToAppIcon,
Expand Down Expand Up @@ -135,6 +137,8 @@ export default {
ArrowBackIcon,
LockPng,
RedLockPng,
MapPinRed,
MapPinGrey,
TaskIcon,
SubmissionIcon,
FeatureIcon,
Expand Down
2 changes: 0 additions & 2 deletions src/frontend/src/store/types/ICreateProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export type CreateProjectStateTypes = {
descriptionToFocus: string | null;
task_num_buildings: number | null;
task_split_dimension: number | null;
new_geom_type: NewGeomTypes;
};
export type ValidateCustomFormResponse = {
detail: { message: string; possible_reason: string };
Expand Down Expand Up @@ -111,7 +110,6 @@ export type ProjectDetailsTypes = {
hasCustomTMS: boolean;
customFormUpload: any;
hasAdditionalFeature: boolean;
new_geom_type: NewGeomTypes;
project_admins: number[];
};

Expand Down
6 changes: 6 additions & 0 deletions src/frontend/src/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export type NewGeomTypes = {
LINESTRING: 'LINESTRING';
};

export enum GeoGeomTypesEnum {
POINT = 'Point',
POLYGON = 'Polygon',
LINESTRING = 'LineString',
}

export enum submission_status {
null = 'Received',
hasIssues = 'Has issues',
Expand Down
123 changes: 66 additions & 57 deletions src/frontend/src/utilfunctions/getTaskStatusStyle.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Fill, Icon, Stroke, Style } from 'ol/style';
import { asArray, asString } from 'ol/color';
import { getCenter } from 'ol/extent';
import { Point } from 'ol/geom';
import AssetModules from '@/shared/AssetModules';
import { entity_state } from '@/types/enums';
import { EntityOsmMap } from '@/store/types/IProject';
import { GeoGeomTypesEnum } from '@/types/enums';

function createPolygonStyle(fillColor: string, strokeColor: string) {
return new Style({
Expand All @@ -18,14 +18,29 @@ function createPolygonStyle(fillColor: string, strokeColor: string) {
});
}

function createIconStyle(iconSrc: string) {
function createFeaturePolygonStyle(color: string, strokeOpacity: number = 1) {
return new Style({
stroke: new Stroke({
color: 'rgb(0,0,0,0.5)',
width: 1,
opacity: strokeOpacity,
}),
fill: new Fill({
color: color,
}),
});
}

function createIconStyle(iconSrc: string, scale: number = 0.8, color: any = 'red') {
return new Style({
image: new Icon({
anchor: [0.5, 1],
scale: 0.8,
scale: scale,
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: iconSrc,
color: color,
opacity: 1,
}),
geometry: function (feature) {
const polygonCentroid = getCenter(feature.getGeometry().getExtent());
Expand All @@ -34,6 +49,11 @@ function createIconStyle(iconSrc: string) {
});
}

function updateRbgAlpha(colorString: string, alphaVal: number) {
const val = asArray(colorString);
return `rgb(${val.slice(0, 3).join(',')},${alphaVal})`;
}

const strokeColor = 'rgb(0,0,0,0.3)';
const secondaryStrokeColor = 'rgb(0,0,0,1)';

Expand Down Expand Up @@ -88,61 +108,50 @@ const getTaskStatusStyle = (feature: Record<string, any>, mapTheme: Record<strin
return geojsonStyles[status];
};

export const getFeatureStatusStyle = (osmId: string, mapTheme: Record<string, any>, entityOsmMap: EntityOsmMap[]) => {
const entity = entityOsmMap?.find((entity) => entity?.osm_id === osmId) as EntityOsmMap;
export const getFeatureStatusStyle = (geomType: string, mapTheme: Record<string, any>, mappingStatus: string) => {
let geojsonStyles;

let status = entity_state[entity?.status];
if (geomType === GeoGeomTypesEnum.POINT) {
geojsonStyles = {
READY: createIconStyle(
AssetModules.MapPinGrey,
1.1,
updateRbgAlpha(mapTheme.palette.entityStatusColors.ready, 1),
),
OPENED_IN_ODK: createIconStyle(
AssetModules.MapPinGrey,
1.1,
updateRbgAlpha(mapTheme.palette.entityStatusColors.opened_in_odk, 1),
),
SURVEY_SUBMITTED: createIconStyle(
AssetModules.MapPinGrey,
1.1,
updateRbgAlpha(mapTheme.palette.entityStatusColors.survey_submitted, 1),
),
MARKED_BAD: createIconStyle(
AssetModules.MapPinGrey,
1.1,
updateRbgAlpha(mapTheme.palette.entityStatusColors.marked_bad, 1),
),
VALIDATED: createIconStyle(
AssetModules.MapPinGrey,
1.1,
updateRbgAlpha(mapTheme.palette.entityStatusColors.validated, 1),
),
};
} else if (geomType === GeoGeomTypesEnum.POLYGON) {
geojsonStyles = {
READY: createFeaturePolygonStyle(mapTheme.palette.entityStatusColors.ready, 0.2),
OPENED_IN_ODK: createFeaturePolygonStyle(mapTheme.palette.entityStatusColors.opened_in_odk, 0.2),
SURVEY_SUBMITTED: createFeaturePolygonStyle(mapTheme.palette.entityStatusColors.survey_submitted),
MARKED_BAD: createFeaturePolygonStyle(mapTheme.palette.entityStatusColors.marked_bad),
VALIDATED: createFeaturePolygonStyle(mapTheme.palette.entityStatusColors.validated),
};
} else if (geomType === GeoGeomTypesEnum.LINESTRING) {
console.log('linestring style not set');
}

const borderStrokeColor = 'rgb(0,0,0,0.5)';

const strokeStyle = new Stroke({
color: borderStrokeColor,
width: 1,
opacity: 0.2,
});

const geojsonStyles = {
READY: new Style({
stroke: strokeStyle,
fill: new Fill({
color: mapTheme.palette.entityStatusColors.ready,
}),
}),
OPENED_IN_ODK: new Style({
stroke: strokeStyle,
fill: new Fill({
color: mapTheme.palette.entityStatusColors.opened_in_odk,
}),
}),
SURVEY_SUBMITTED: new Style({
stroke: new Stroke({
color: borderStrokeColor,
width: 1,
}),
fill: new Fill({
color: mapTheme.palette.entityStatusColors.survey_submitted,
}),
}),
MARKED_BAD: new Style({
stroke: new Stroke({
color: borderStrokeColor,
width: 1,
}),
fill: new Fill({
color: mapTheme.palette.entityStatusColors.marked_bad,
}),
}),
VALIDATED: new Style({
stroke: new Stroke({
color: borderStrokeColor,
width: 1,
}),
fill: new Fill({
color: mapTheme.palette.entityStatusColors.validated,
}),
}),
};
return geojsonStyles[status];
return geojsonStyles[mappingStatus];
};

export default getTaskStatusStyle;
11 changes: 9 additions & 2 deletions src/frontend/src/views/ProjectDetailsV2.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import '../../node_modules/ol/ol.css';
import '../styles/home.scss';
import { Style, Stroke } from 'ol/style';
import WindowDimension from '@/hooks/WindowDimension';
import ActivitiesPanel from '@/components/ProjectDetailsV2/ActivitiesPanel';
import { ProjectById, GetEntityStatusList, GetGeometryLog, SyncTaskState } from '@/api/Project';
Expand Down Expand Up @@ -35,8 +36,9 @@ import Comments from '@/components/ProjectDetailsV2/Comments';
import { Geolocation } from '@/utilfunctions/Geolocation';
import Instructions from '@/components/ProjectDetailsV2/Instructions';
import useDocumentTitle from '@/utilfunctions/useDocumentTitle';
import { Style, Stroke } from 'ol/style';
import MapStyles from '@/hooks/MapStyles';
import { EntityOsmMap } from '@/store/types/IProject';
import { entity_state } from '@/types/enums';

const ProjectDetailsV2 = () => {
useDocumentTitle('Project Details');
Expand Down Expand Up @@ -517,7 +519,12 @@ const ProjectDetailsV2 = () => {
fgbUrl={dataExtractUrl}
fgbExtent={dataExtractExtent}
getTaskStatusStyle={(feature) => {
return getFeatureStatusStyle(feature?.getProperties()?.osm_id, mapTheme, entityOsmMap);
const geomType = feature.getGeometry().getType();
const entity = entityOsmMap?.find(
(entity) => entity?.osm_id === feature?.getProperties()?.osm_id,
) as EntityOsmMap;
const status = entity_state[entity?.status];
return getFeatureStatusStyle(geomType, mapTheme, status);
}}
viewProperties={{
size: map?.getSize(),
Expand Down
Binary file added src/mapper/src/assets/images/map-pin-blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/mapper/src/assets/images/map-pin-green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/mapper/src/assets/images/map-pin-grey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/mapper/src/assets/images/map-pin-red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/mapper/src/assets/images/map-pin-yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit ceb31a3

Please sign in to comment.