diff --git a/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx b/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx
index 3cd2ce1a37..cbd2c91dbf 100644
--- a/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx
+++ b/src/frontend/src/components/ProjectDetailsV2/FeatureSelectionPopup.tsx
@@ -19,7 +19,9 @@ const FeatureSelectionPopup = ({ featureProperties, taskId }: FeatureSelectionPo
const taskModalStatus = useAppSelector((state) => state.project.taskModalStatus);
const entityOsmMap = useAppSelector((state) => state.project.entityOsmMap);
const projectId = params.id || '';
- const entity = entityOsmMap.find((x) => x.osm_id === featureProperties?.osm_id);
+ const entity = entityOsmMap.find(
+ (x) => x.osm_id === featureProperties?.osm_id || x.id === featureProperties?.entity_id,
+ );
const submissionIds = entity?.submission_ids ? entity?.submission_ids?.split(',') : [];
return (
@@ -36,7 +38,9 @@ const FeatureSelectionPopup = ({ featureProperties, taskId }: FeatureSelectionPo
} fmtm-rounded-t-2xl md:fmtm-rounded-tr-none md:fmtm-rounded-l-2xl`}
>
-
-
- Tags:
- {featureProperties?.tags}
-
-
- Timestamp:
- {featureProperties?.timestamp}
-
-
- Changeset:
- {featureProperties?.changeset}
-
-
- Version:
- {featureProperties?.version}
-
+ {featureProperties?.changeset && (
+
+
+
+ Tags:
+ {featureProperties?.tags}
+
+
+ Timestamp:
+ {featureProperties?.timestamp}
+
+
+ Changeset:
+ {featureProperties?.changeset}
+
+
+ Version:
+ {featureProperties?.version}
+
+
-
+ )}
{!submissionIds ||
(submissionIds?.length !== 0 && (
diff --git a/src/frontend/src/store/types/ITask.ts b/src/frontend/src/store/types/ITask.ts
index 89875694b4..690622e501 100644
--- a/src/frontend/src/store/types/ITask.ts
+++ b/src/frontend/src/store/types/ITask.ts
@@ -18,9 +18,10 @@ type downloadSubmissionLoadingTypes = {
};
export type TaskFeatureSelectionProperties = {
- osm_id: number;
- tags: string;
- timestamp: string;
- version: number;
- changeset: number;
+ osm_id?: number;
+ tags?: string;
+ timestamp?: string;
+ version?: number;
+ changeset?: number;
+ entity_id?: string;
};
diff --git a/src/frontend/src/utilfunctions/getTaskStatusStyle.ts b/src/frontend/src/utilfunctions/getTaskStatusStyle.ts
index 6edb289658..c18b0f0551 100644
--- a/src/frontend/src/utilfunctions/getTaskStatusStyle.ts
+++ b/src/frontend/src/utilfunctions/getTaskStatusStyle.ts
@@ -2,8 +2,7 @@ import { Fill, Icon, Stroke, Style } from 'ol/style';
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 { Text } from 'ol/style';
function createPolygonStyle(fillColor: string, strokeColor: string) {
return new Style({
@@ -106,11 +105,7 @@ const getTaskStatusStyle = (feature: Record, mapTheme: Record, entityOsmMap: EntityOsmMap[]) => {
- const entity = entityOsmMap?.find((entity) => entity?.osm_id === osmId) as EntityOsmMap;
-
- let status = entity_state[entity?.status];
-
+export const getFeatureStatusStyle = (mapTheme: Record, status: string, osm_id: number) => {
const borderStrokeColor = 'rgb(0,0,0,0.5)';
const strokeStyle = new Stroke({
@@ -125,6 +120,10 @@ export const getFeatureStatusStyle = (osmId: string, mapTheme: Record {
useDocumentTitle('Project Details');
@@ -71,6 +73,7 @@ const ProjectDetailsV2 = () => {
const entityOsmMap = useAppSelector((state) => state?.project?.entityOsmMap);
const entityOsmMapLoading = useAppSelector((state) => state?.project?.entityOsmMapLoading);
const badGeomFeatureCollection = useAppSelector((state) => state?.project?.badGeomFeatureCollection);
+ const newGeomFeatureCollection = useAppSelector((state) => state?.project?.newGeomFeatureCollection);
const getGeomLogLoading = useAppSelector((state) => state?.project?.getGeomLogLoading);
const syncTaskStateLoading = useAppSelector((state) => state?.project?.syncTaskStateLoading);
@@ -512,12 +515,36 @@ const ProjectDetailsV2 = () => {
zIndex={5}
style=""
/>
+ {
+ const entity = entityOsmMap?.find(
+ (entity) => entity?.id === feature?.getProperties()?.entity_id,
+ ) as EntityOsmMap;
+ const status = entity_state[entity?.status];
+ return getFeatureStatusStyle(mapTheme, status, entity?.osm_id);
+ }}
+ />
{dataExtractUrl && isValidUrl(dataExtractUrl) && dataExtractExtent && selectedTask && (
{
- return getFeatureStatusStyle(feature?.getProperties()?.osm_id, mapTheme, entityOsmMap);
+ const osmId = feature?.getProperties()?.osm_id;
+ const entity = entityOsmMap?.find((entity) => entity?.osm_id === osmId) as EntityOsmMap;
+
+ const status = entity_state[entity?.status];
+ return getFeatureStatusStyle(mapTheme, status, entity?.osm_id);
}}
viewProperties={{
size: map?.getSize(),
diff --git a/src/mapper/src/lib/components/map/main.svelte b/src/mapper/src/lib/components/map/main.svelte
index f7aa006825..9d06a0ae5c 100644
--- a/src/mapper/src/lib/components/map/main.svelte
+++ b/src/mapper/src/lib/components/map/main.svelte
@@ -155,6 +155,9 @@
const clickedTaskFeature = map?.queryRenderedFeatures(e.point, {
layers: ['task-fill-layer'],
});
+ const clickedNewEntityFeature = map?.queryRenderedFeatures(e.point, {
+ layers: ['new-entity-polygon-layer'],
+ });
// if clicked point contains entity then set it's osm id else set null to store
if (clickedEntityFeature && clickedEntityFeature?.length > 0) {
const entityCentroid = centroid(clickedEntityFeature[0].geometry);
@@ -164,6 +167,14 @@
entityId: clickedEntityId,
coordinate: entityCentroid?.geometry?.coordinates,
});
+ } else if (clickedNewEntityFeature && clickedNewEntityFeature?.length > 0) {
+ const entityCentroid = centroid(clickedNewEntityFeature[0].geometry);
+ const clickedEntityId = clickedNewEntityFeature[0]?.properties?.entity_id;
+ entitiesStore.setSelectedEntity(clickedEntityId);
+ entitiesStore.setSelectedEntityCoordinate({
+ entityId: clickedEntityId,
+ coordinate: entityCentroid?.geometry?.coordinates,
+ });
} else {
entitiesStore.setSelectedEntity(null);
entitiesStore.setSelectedEntityCoordinate(null);
@@ -172,15 +183,18 @@
// if clicked point contains task layer
if (clickedTaskFeature && clickedTaskFeature?.length > 0) {
taskAreaClicked = true;
- const clickedTaskId = clickedTaskFeature[0]?.properties?.fid;
- taskStore.setSelectedTaskId(clickedTaskId, clickedTaskFeature[0]?.properties?.task_index);
+ const clickedTaskId = clickedTaskFeature?.[0]?.properties?.fid;
+ taskStore.setSelectedTaskId(clickedTaskId, clickedTaskFeature?.[0]?.properties?.task_index);
if (+(projectSetupStepStore.projectSetupStep || 0) === projectSetupStepEnum['task_selection']) {
localStorage.setItem(`project-${projectId}-setup`, projectSetupStepEnum['complete_setup']);
projectSetupStepStore.setProjectSetupStep(projectSetupStepEnum['complete_setup']);
}
}
- if (clickedEntityFeature && clickedEntityFeature?.length > 0) {
+ if (
+ (clickedEntityFeature && clickedEntityFeature?.length > 0) ||
+ (clickedNewEntityFeature && clickedNewEntityFeature?.length > 0)
+ ) {
toggleActionModal('entity-modal');
} else if (clickedTaskFeature && clickedTaskFeature?.length > 0) {
toggleActionModal('task-modal');
@@ -267,21 +281,42 @@
}
});
- function addStatusToGeojsonProperty(geojsonData: FeatureCollection): FeatureCollection {
- return {
- ...geojsonData,
- features: geojsonData.features.map((feature) => {
- const entity = entitiesStore.entitiesStatusList.find((entity) => entity.osmid === feature?.properties?.osm_id);
- return {
- ...feature,
- properties: {
- ...feature.properties,
- status: entity?.status,
- entity_id: entity?.entity_id,
- },
- };
- }),
- };
+ function addStatusToGeojsonProperty(geojsonData: FeatureCollection, entityType: '' | 'new'): FeatureCollection {
+ if (entityType === 'new') {
+ return {
+ ...geojsonData,
+ features: geojsonData.features.map((feature) => {
+ const entity = entitiesStore.entitiesStatusList.find(
+ (entity) => entity.entity_id === feature?.properties?.entity_id,
+ );
+ return {
+ ...feature,
+ properties: {
+ ...feature.properties,
+ status: entity?.status,
+ entity_id: entity?.entity_id,
+ },
+ };
+ }),
+ };
+ } else {
+ return {
+ ...geojsonData,
+ features: geojsonData.features.map((feature) => {
+ const entity = entitiesStore.entitiesStatusList.find(
+ (entity) => entity.osmid === feature?.properties?.osm_id,
+ );
+ return {
+ ...feature,
+ properties: {
+ ...feature.properties,
+ status: entity?.status,
+ entity_id: entity?.entity_id,
+ },
+ };
+ }),
+ };
+ }
}
function zoomToProject() {
@@ -487,7 +522,7 @@
extent={taskStore.selectedTaskGeom}
extractGeomCols={true}
promoteId="id"
- processGeojson={(geojsonData) => addStatusToGeojsonProperty(geojsonData)}
+ processGeojson={(geojsonData) => addStatusToGeojsonProperty(geojsonData, '')}
geojsonUpdateDependency={entitiesStore.entitiesStatusList}
>
{/if}
-
+ {#if showEntityLayer}
+
+
+
+
+ {/if}
@@ -624,7 +708,7 @@
(selectedStyleUrl = style)}
diff --git a/src/mapper/src/lib/odk/collect.ts b/src/mapper/src/lib/odk/collect.ts
index eab0c8769f..56ac972849 100644
--- a/src/mapper/src/lib/odk/collect.ts
+++ b/src/mapper/src/lib/odk/collect.ts
@@ -5,18 +5,16 @@ import { geojsonGeomToJavarosa } from '$lib/odk/javarosa';
const alertStore = getAlertStore();
-export function openOdkCollectNewFeature(xFormId: string, geom: GeoJSONGeometry, task_id: number | null) {
- if (!xFormId || !geom) {
+export function openOdkCollectNewFeature(xFormId: string, entityId: string) {
+ if (!xFormId || !entityId) {
return;
}
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
- const javarosaGeom = geojsonGeomToJavarosa(geom);
-
if (isMobile) {
// TODO we need to update the form to support task_id=${}&
- document.location.href = `odkcollect://form/${xFormId}?new_feature=${javarosaGeom}&task_id=${task_id}`;
+ document.location.href = `odkcollect://form/${xFormId}?feature=${entityId}`;
} else {
alertStore.setAlert({
variant: 'warning',
diff --git a/src/mapper/src/routes/[projectId]/+page.svelte b/src/mapper/src/routes/[projectId]/+page.svelte
index 6322a9e8df..c26f55af6c 100644
--- a/src/mapper/src/routes/[projectId]/+page.svelte
+++ b/src/mapper/src/routes/[projectId]/+page.svelte
@@ -23,7 +23,7 @@
import { getTaskStore, getTaskEventStream } from '$store/tasks.svelte.ts';
import { getEntitiesStatusStore, getEntityStatusStream, getNewBadGeomStream } from '$store/entities.svelte.ts';
import More from '$lib/components/more/index.svelte';
- import { getProjectSetupStepStore, getCommonStore } from '$store/common.svelte.ts';
+ import { getProjectSetupStepStore, getCommonStore, getAlertStore } from '$store/common.svelte.ts';
import { projectSetupStep as projectSetupStepEnum } from '$constants/enums.ts';
import type { ShapeStream } from '@electric-sql/client';
@@ -45,6 +45,7 @@
const taskStore = getTaskStore();
const entitiesStore = getEntitiesStatusStore();
const commonStore = getCommonStore();
+ const alertStore = getAlertStore();
const taskEventStream = getTaskEventStream(data.projectId);
const entityStatusStream = getEntityStatusStream(data.projectId);
@@ -114,15 +115,17 @@
});
$effect(() => {
+ entitiesStore.syncEntityStatus(data.projectId);
+ });
+
+ $effect(() => {
+ entitiesStore.entitiesList;
let entityStatusStream: ShapeStream | undefined;
+ if (entitiesStore.entitiesList?.length === 0) return;
async function getEntityStatus() {
- const entityStatusResponse = await fetch(`${API_URL}/projects/${data.projectId}/entities/statuses`, {
- credentials: 'include',
- });
- const response = await entityStatusResponse.json();
entityStatusStream = getEntityStatusStream(data.projectId);
- await entitiesStore.subscribeToEntityStatusUpdates(entityStatusStream, response);
+ await entitiesStore.subscribeToEntityStatusUpdates(entityStatusStream, entitiesStore.entitiesList);
}
getEntityStatus();
@@ -159,10 +162,30 @@
newFeatureGeom = null;
}
- function mapNewFeatureInODK() {
- const newGeom = newFeatureGeom;
- cancelMapNewFeatureInODK();
- openOdkCollectNewFeature(data?.project?.odk_form_id, newGeom, taskStore.selectedTaskId);
+ async function mapNewFeatureInODK() {
+ {
+ /*
+ 1: create entity in ODK of newly created feature
+ 2: create geom record to show the feature on map
+ 3: pass entity uuid to ODK intent URL as a param
+ */
+ }
+ try {
+ const entity = await entitiesStore.createEntity(data.projectId, {
+ type: 'FeatureCollection',
+ features: [{ type: 'Feature', geometry: newFeatureGeom, properties: {} }],
+ });
+ await entitiesStore.createGeomRecord(data.projectId, {
+ status: 'NEW',
+ geojson: { type: 'Feature', geometry: newFeatureGeom, properties: { entity_id: entity.uuid } },
+ project_id: data.projectId,
+ });
+ entitiesStore.syncEntityStatus(data.projectId);
+ cancelMapNewFeatureInODK();
+ openOdkCollectNewFeature(data?.project?.odk_form_id, entity.uuid);
+ } catch (error) {
+ alertStore.setAlert({ message: 'Unable to create entity', variant: 'danger' });
+ }
}
@@ -229,6 +252,7 @@
tabindex="0"
size="small"
class="primary w-fit"
+ loading={entitiesStore.createEntityLoading || entitiesStore.createGeomRecordLoading}
>
PROCEED
diff --git a/src/mapper/src/store/entities.svelte.ts b/src/mapper/src/store/entities.svelte.ts
index 70898d008a..8bf6797b94 100644
--- a/src/mapper/src/store/entities.svelte.ts
+++ b/src/mapper/src/store/entities.svelte.ts
@@ -2,6 +2,7 @@ import { ShapeStream, Shape } from '@electric-sql/client';
import type { ShapeData } from '@electric-sql/client';
import type { Feature, FeatureCollection } from 'geojson';
import type { LngLatLike } from 'svelte-maplibre';
+import { getAlertStore } from './common.svelte';
const API_URL = import.meta.env.VITE_API_URL;
@@ -53,6 +54,10 @@ let updateEntityStatusLoading: boolean = $state(false);
let selectedEntityCoordinate: entityIdCoordinateMapType | null = $state(null);
let entityToNavigate: entityIdCoordinateMapType | null = $state(null);
let toggleGeolocation: boolean = $state(false);
+let createEntityLoading: boolean = $state(false);
+let createGeomRecordLoading: boolean = $state(false);
+let entitiesList: entitiesListType[] = $state([]);
+let alertStore = getAlertStore();
function getEntityStatusStream(projectId: number): ShapeStream | undefined {
if (!projectId) {
@@ -119,7 +124,7 @@ function getEntitiesStatusStore() {
});
}
- async function setSelectedEntity(entityOsmId: number | null) {
+ async function setSelectedEntity(entityOsmId: string | null) {
selectedEntity = entityOsmId;
}
@@ -130,9 +135,11 @@ function getEntitiesStatusStore() {
async function syncEntityStatus(projectId: number) {
try {
syncEntityStatusLoading = true;
- await fetch(`${API_URL}/projects/${projectId}/entities/statuses`, {
+ const entityStatusResponse = await fetch(`${API_URL}/projects/${projectId}/entities/statuses`, {
credentials: 'include',
});
+ const response = await entityStatusResponse.json();
+ entitiesList = response;
syncEntityStatusLoading = false;
} catch (error) {
syncEntityStatusLoading = false;
@@ -156,6 +163,57 @@ function getEntitiesStatusStore() {
}
}
+ async function createEntity(projectId: number, payload: Record) {
+ try {
+ createEntityLoading = true;
+ const resp = await fetch(`${import.meta.env.VITE_API_URL}/projects/${projectId}/create-entity`, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ credentials: 'include',
+ });
+ if (!resp.ok) {
+ const errorData = await resp.json();
+ throw new Error(errorData.detail);
+ }
+ return await resp.json();
+ } catch (error: any) {
+ alertStore.setAlert({
+ variant: 'danger',
+ message: error.message || 'Failed to create entity',
+ });
+ } finally {
+ createEntityLoading = false;
+ }
+ }
+
+ async function createGeomRecord(projectId: number, payload: Record) {
+ try {
+ createGeomRecordLoading = true;
+ const resp = await fetch(`${import.meta.env.VITE_API_URL}/projects/${projectId}/geometry/records`, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ credentials: 'include',
+ });
+ if (!resp.ok) {
+ const errorData = await resp.json();
+ throw new Error(errorData.detail);
+ }
+ } catch (error: any) {
+ alertStore.setAlert({
+ variant: 'danger',
+ message: error.message || 'Failed to create geometry record',
+ });
+ } finally {
+ createGeomRecordLoading = false;
+ }
+ }
+
function setEntityToNavigate(entityCoordinate: entityIdCoordinateMapType | null) {
entityToNavigate = entityCoordinate;
}
@@ -173,6 +231,8 @@ function getEntitiesStatusStore() {
setSelectedEntity: setSelectedEntity,
syncEntityStatus: syncEntityStatus,
updateEntityStatus: updateEntityStatus,
+ createEntity: createEntity,
+ createGeomRecord: createGeomRecord,
setSelectedEntityCoordinate: setSelectedEntityCoordinate,
setEntityToNavigate: setEntityToNavigate,
setToggleGeolocation: setToggleGeolocation,
@@ -208,6 +268,15 @@ function getEntitiesStatusStore() {
get userLocationCoord() {
return userLocationCoord;
},
+ get createEntityLoading() {
+ return createEntityLoading;
+ },
+ get createGeomRecordLoading() {
+ return createGeomRecordLoading;
+ },
+ get entitiesList() {
+ return entitiesList;
+ },
};
}