diff --git a/app/src/state/actions/whatsHere/WhatsHere.ts b/app/src/state/actions/whatsHere/WhatsHere.ts index b3054573d..114e6adf2 100644 --- a/app/src/state/actions/whatsHere/WhatsHere.ts +++ b/app/src/state/actions/whatsHere/WhatsHere.ts @@ -29,7 +29,7 @@ class WhatsHere { static readonly page_activity = createAction<{ page: number; limit: number }>(`${this.PREFIX}/page_activity`); static readonly server_filtered_ids_fetched = createAction( `${this.PREFIX}/server_filtered_ids_fetched`, - (activities, iapp) => ({ + (activities: string[], iapp: string[]) => ({ payload: { activities, iapp } }) ); diff --git a/app/src/state/reducers/map.ts b/app/src/state/reducers/map.ts index 5227cbd2e..1e60d0e8a 100644 --- a/app/src/state/reducers/map.ts +++ b/app/src/state/reducers/map.ts @@ -532,34 +532,22 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma } else if (WhatsHere.server_filtered_ids_fetched.match(action)) { draftState.whatsHere.serverActivityIDs = action.payload.activities; draftState.whatsHere.serverIAPPIDs = action.payload.iapp; - const toggledOnActivityLayers = draftState.layers.filter( - (layer) => layer.type === RecordSetType.Activity && layer.layerState.mapToggle + ({ type, layerState }) => type === RecordSetType.Activity && layerState.mapToggle ); const toggledOnIAPPLayers = draftState.layers.filter( - (layer) => layer.type === RecordSetType.IAPP && layer.layerState.mapToggle + ({ type, layerState }) => type === RecordSetType.IAPP && layerState.mapToggle ); + const localActivityIDs = toggledOnActivityLayers.flatMap( + (layer) => layer.IDList ?? layer?.layerState?.cacheMetadata?.idList ?? [] + ); + const localIappIds = toggledOnIAPPLayers.flatMap((layer) => layer.IDList ?? layer?.cacheMetadata?.idList ?? []); + const iappIds = localIappIds.filter((l) => draftState.whatsHere.serverIAPPIDs.includes(l)); + const activityIds = localActivityIDs.filter((l) => draftState.whatsHere.serverActivityIDs.includes(l)); - let localActivityIDs = []; - - toggledOnActivityLayers.forEach((layer) => { - localActivityIDs = localActivityIDs.concat(layer.IDList); - }); - - let localIAPPIDs = []; - - toggledOnIAPPLayers.forEach((layer) => { - localIAPPIDs = localIAPPIDs.concat(layer.IDList); - }); - - const iappIDs = []; - const activityIDs = []; - localIAPPIDs.forEach((l) => draftState.whatsHere.serverIAPPIDs.includes(l) && iappIDs.push(l)); - localActivityIDs.forEach((l) => draftState.whatsHere.serverActivityIDs.includes(l) && activityIDs.push(l)); - - draftState.whatsHere.ActivityIDs = Array.from(new Set(activityIDs)); - draftState.whatsHere.IAPPIDs = Array.from(new Set(iappIDs)); + draftState.whatsHere.ActivityIDs = Array.from(new Set(activityIds)); + draftState.whatsHere.IAPPIDs = Array.from(new Set(iappIds)); } else if (WhatsHere.sort_filter_update.match(action)) { if (action.payload.type === RecordSetType.IAPP) { draftState.whatsHere.IAPPPage = 0; @@ -758,8 +746,6 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma if (draftState.MapMode === 'VECTOR_ENDPOINT') { draftState.layers[index].loading = false; } - - //if (draftState.activitiesGeoJSON?.features?.length > 0) { if (draftState.MapMode !== 'VECTOR_ENDPOINT' && draftState.activitiesGeoJSONDict !== undefined) { GeoJSONFilterSetForLayer( draftState, diff --git a/app/src/state/sagas/map.ts b/app/src/state/sagas/map.ts index 721a42ccf..b99f222ca 100644 --- a/app/src/state/sagas/map.ts +++ b/app/src/state/sagas/map.ts @@ -1,4 +1,5 @@ -import * as turf from '@turf/turf'; +import { bboxPolygon, Feature, buffer } from '@turf/turf'; +import booleanIntersects from '@turf/boolean-intersects'; import { all, call, debounce, fork, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects'; import { getSearchCriteriaFromFilters } from '../../utils/miscYankedFromComponents'; import { @@ -7,7 +8,6 @@ import { ACTIVITIES_GEOJSON_REFETCH_ONLINE, ACTIVITIES_GET_IDS_FOR_RECORDSET_ONLINE, ACTIVITIES_GET_IDS_FOR_RECORDSET_REQUEST, - ACTIVITIES_GET_IDS_FOR_RECORDSET_SUCCESS, ACTIVITIES_TABLE_ROWS_GET_ONLINE, ACTIVITIES_TABLE_ROWS_GET_REQUEST, ACTIVITY_UPDATE_GEO_REQUEST, @@ -21,7 +21,6 @@ import { IAPP_GEOJSON_GET_SUCCESS, IAPP_GET_IDS_FOR_RECORDSET_ONLINE, IAPP_GET_IDS_FOR_RECORDSET_REQUEST, - IAPP_GET_IDS_FOR_RECORDSET_SUCCESS, IAPP_TABLE_ROWS_GET_ONLINE, IAPP_TABLE_ROWS_GET_REQUEST, INIT_SERVER_BOUNDARIES_GET, @@ -72,7 +71,7 @@ import { InvasivesAPI_Call } from 'hooks/useInvasivesApi'; import { TRACKING_SAGA_HANDLERS } from 'state/sagas/map/tracking'; import WhatsHere from 'state/actions/whatsHere/WhatsHere'; import Prompt from 'state/actions/prompts/Prompt'; -import { RecordSetType, UserRecordSet } from 'interfaces/UserRecordSet'; +import { RecordSetType, UserRecordCacheStatus, UserRecordSet } from 'interfaces/UserRecordSet'; import UserSettings from 'state/actions/userSettings/UserSettings'; import { SortFilter } from 'interfaces/filterParams'; import Activity from 'state/actions/activity/Activity'; @@ -82,6 +81,11 @@ import { LAYER_ELIGIBILITY_UPDATE } from 'state/sagas/map/layer-eligibility'; import { RECORD_COLOURS } from 'constants/colors'; import { PayloadAction } from '@reduxjs/toolkit'; import { IRemoveFilter, IUpdateFilter } from 'state/actions/userSettings/RecordSet'; +import { selectNetworkConnected, selectNetworkState } from 'state/reducers/network'; +import UserRecord from 'interfaces/UserRecord'; +import { MOBILE } from 'state/build-time-config'; +import { RecordCacheServiceFactory } from 'utils/record-cache/context'; +import bboxToPolygon from 'utils/bboxToPolygon'; function* handle_USER_SETTINGS_GET_INITIAL_STATE_SUCCESS(action) { yield put({ type: MAP_INIT_REQUEST, payload: {} }); @@ -112,124 +116,74 @@ function* refetchServerBoundaries() { yield put({ type: INIT_SERVER_BOUNDARIES_GET, payload: { data: shapes } }); } -function* handle_WHATS_HERE_FEATURE(action) { - let mapState = yield select(selectMap); +function* handle_WHATS_HERE_FEATURE(whatsHereFeature: PayloadAction) { + const { MapMode, activitiesGeoJSONDict, IAPPGeoJSONDict } = yield select(selectMap); + const { connected } = yield select(selectNetworkState); - let layersLoading = true; - while (layersLoading) { - mapState = yield select(selectMap); - - const toggledOnActivityLayers = mapState.layers.filter((layer) => { - return layer.layerState.mapToggle && layer.type === RecordSetType.Activity; - }); - - const activityLayersLoading = toggledOnActivityLayers.filter((layer) => { - return layer.loading; - }); - - const toggledOnIAPPLayers = mapState.layers.filter((layer) => { - return layer.layerState.mapToggle && layer.type === RecordSetType.IAPP; - }); - - const IAPPLayersLoading = toggledOnIAPPLayers.filter((layer) => { - return layer.loading; - }); - - if (activityLayersLoading.length === 0 && IAPPLayersLoading.length === 0) { - layersLoading = false; - } else { - const actionsToTake = []; - if (activityLayersLoading.length > 0) { - actionsToTake.push( - activityLayersLoading.map((layer) => { - return ACTIVITIES_GET_IDS_FOR_RECORDSET_SUCCESS; - }) - ); - } - if (IAPPLayersLoading.length > 0) { - actionsToTake.push( - IAPPLayersLoading.map((layer) => { - return IAPP_GET_IDS_FOR_RECORDSET_SUCCESS; - }) - ); - } - yield all(actionsToTake.map((action) => take(action))); - } - } - if (mapState.MapMode === 'VECTOR_ENDPOINT') { - // get all the toggled on recordsets - - const activitiesfilterObj = { - selectColumns: ['activity_id'], - tableFilters: [ + if (MapMode === 'VECTOR_ENDPOINT') { + if (connected) { + // get all the toggled on recordsets + const tableFilters = [ { id: '0.81778552637744651712083357942', filterType: 'spatialFilterDrawn', operator: 'CONTAINED IN', filter: '0.652479498272151712093656568', - geojson: action.payload + geojson: whatsHereFeature.payload } - ], - limit: 200000 - }; - - const activitiesNetworkReturn = yield InvasivesAPI_Call('POST', `/api/v2/activities/`, { - filterObjects: [activitiesfilterObj] - }); - - let activitiesServerIDList = []; - if (activitiesNetworkReturn.data.result || activitiesNetworkReturn.data?.data?.result) { - const list = activitiesNetworkReturn.data?.data?.result - ? activitiesNetworkReturn.data?.data?.result - : activitiesNetworkReturn.data?.result; - activitiesServerIDList = list.map((row) => { - return row.activity_id; - }); - } - - const iappfilterObj = { - selectColumns: ['site_id'], - tableFilters: [ - { - id: '0.81778552637744651712083357942', - filterType: 'spatialFilterDrawn', - operator: 'CONTAINED IN', - filter: '0.652479498272151712093656568', - geojson: action.payload - } - ], - limit: 200000 - }; - - const iappNetworkReturn = yield InvasivesAPI_Call('POST', `/api/v2/iapp/`, { - filterObjects: [iappfilterObj] - }); - - let iappServerIDList = []; + ]; - if (iappNetworkReturn.data.result || iappNetworkReturn.data?.data?.result) { - const list = iappNetworkReturn.data?.data?.result - ? iappNetworkReturn.data?.data?.result - : iappNetworkReturn.data?.result; - iappServerIDList = list.map((row) => { - return row.site_id; + const activitiesfilterObj = { + selectColumns: ['activity_id'], + tableFilters, + limit: 200000 + }; + const iappfilterObj = { + selectColumns: ['site_id'], + tableFilters, + limit: 200000 + }; + const [activitiesNetworkReturn, iappNetworkReturn] = yield all([ + call(InvasivesAPI_Call, 'POST', `/api/v2/activities/`, { + filterObjects: [activitiesfilterObj] + }), + call(InvasivesAPI_Call, 'POST', `/api/v2/iapp/`, { + filterObjects: [iappfilterObj] + }) + ]); + + const activityReturn = activitiesNetworkReturn?.data?.data?.result ?? activitiesNetworkReturn?.data?.result ?? []; + const activitiesServerIDList: string[] = activityReturn.map((row: UserRecord) => row.activity_id); + + const iappReturn = iappNetworkReturn?.data?.data?.result ?? iappNetworkReturn?.data?.result ?? []; + const iappServerIDList: string[] = iappReturn.map((row: Record) => row.site_id); + yield put(WhatsHere.server_filtered_ids_fetched(activitiesServerIDList, iappServerIDList)); + } else { + // Get IDs from Offline Caches + const { recordSets } = yield select(selectUserSettings); + const recordSetsInBoundingBox = Object.keys(recordSets).filter((set) => { + const { bbox, status } = recordSets[set].cacheMetadata; + const recordSetIsCached = status === UserRecordCacheStatus.CACHED; + return recordSetIsCached && bbox && booleanIntersects(whatsHereFeature.payload, bboxToPolygon(bbox)); }); + const overlappingRecords: string[] = []; + recordSetsInBoundingBox.flatMap((set) => + recordSets[set].cacheMetadata.cachedGeoJson.data.features.forEach((shape: Feature) => { + if (booleanIntersects(whatsHereFeature.payload, shape)) { + overlappingRecords.push(shape?.properties?.description); + } + }) + ); + yield put(WhatsHere.server_filtered_ids_fetched(overlappingRecords, [])); } - - yield put(WhatsHere.server_filtered_ids_fetched(activitiesServerIDList, iappServerIDList)); - } - - if (mapState.MapMode !== 'VECTOR_ENDPOINT') { - if (!mapState.activitiesGeoJSONDict) { + } else { + if (!activitiesGeoJSONDict) { yield take(ACTIVITIES_GEOJSON_GET_SUCCESS); } - mapState = yield select(selectMap); - if (!mapState.IAPPGeoJSONDict) { + if (!IAPPGeoJSONDict) { yield take(IAPP_GEOJSON_GET_SUCCESS); } - - yield put(WhatsHere.map_init_get_activity()); - yield put(WhatsHere.map_init_get_poi()); + yield all([put(WhatsHere.map_init_get_activity()), put(WhatsHere.map_init_get_poi())]); } } @@ -317,14 +271,15 @@ function* handle_WHATS_HERE_PAGE_POI(action) { yield put(WhatsHere.iapp_rows_request()); } -function* handle_WHATS_HERE_ACTIVITY_ROWS_REQUEST(action) { +function* handle_WHATS_HERE_ACTIVITY_ROWS_REQUEST() { const mapState = yield select(selectMap); - if (mapState.MapMode === 'VECTOR_ENDPOINT') { - const startRecord = - mapState?.whatsHere?.ActivityLimit * (mapState?.whatsHere?.ActivityPage + 1) - mapState?.whatsHere?.ActivityLimit; - const endRecord = mapState?.whatsHere?.ActivityLimit * (mapState?.whatsHere?.ActivityPage + 1); - const slicedIDs = mapState.whatsHere.ActivityIDs.slice(startRecord, endRecord); + const connected = yield select(selectNetworkConnected); + if (mapState.MapMode === 'VECTOR_ENDPOINT') { + const { whatsHere } = mapState; + const startRecord = whatsHere.ActivityLimit * (whatsHere.ActivityPage + 1) - whatsHere.ActivityLimit; + const endRecord = whatsHere.ActivityLimit * (whatsHere.ActivityPage + 1); + const slicedIDs = whatsHere.ActivityIDs.slice(startRecord, endRecord); const filterObject = { selectColumns: [ 'activity_id', @@ -342,28 +297,37 @@ function* handle_WHATS_HERE_ACTIVITY_ROWS_REQUEST(action) { return; } - const networkReturn = yield InvasivesAPI_Call('POST', `/api/v2/activities/`, { - filterObjects: [filterObject] - }); - - const mappedToWhatsHereColumns = networkReturn.data.result.map((activityRecord) => { + let records: UserRecord[]; + if (MOBILE && !connected) { + const service = yield RecordCacheServiceFactory.getPlatformInstance(); + records = yield service.fetchPaginatedCachedRecords( + whatsHere.ActivityIDs, + whatsHere.ActivityPage, + whatsHere.ActivityLimit + ); + } else { + const networkReturn = yield InvasivesAPI_Call('POST', `/api/v2/activities/`, { + filterObjects: [filterObject] + }); + records = networkReturn.data.result; + } + const mappedToWhatsHereColumns = records.map((activityRecord) => { + // Differenciate the Cached records from the API called ones + const shortHand = activityRecord.activity_payload ? activityRecord.activity_payload : activityRecord; return { id: activityRecord.activity_id, short_id: activityRecord.short_id, activity_type: activityRecord.activity_type, jurisdiction_code: activityRecord.jurisdiction_display, species_code: activityRecord.map_symbol, - reported_area: activityRecord.activity_payload.form_data.activity_data.reported_area, - geometry: activityRecord.activity_payload.geometry?.[0], - created: new Date(activityRecord.activity_payload.form_data.activity_data.activity_date_time).toDateString() + reported_area: shortHand.form_data.activity_data.reported_area, + geometry: shortHand.geometry?.[0], + created: new Date(shortHand.form_data.activity_data.activity_date_time).toDateString() }; }); yield put(WhatsHere.activity_rows_success(mappedToWhatsHereColumns)); - } - - if (mapState.MapMode !== 'VECTOR_ENDPOINT') { + } else { try { - const mapState = yield select(selectMap); const startRecord = mapState?.whatsHere?.ActivityLimit * (mapState?.whatsHere?.ActivityPage + 1) - mapState?.whatsHere?.ActivityLimit; @@ -521,20 +485,18 @@ function* handle_RECORD_SET_TO_EXCEL_REQUEST(action) { } } -function* handle_WHATS_HERE_SORT_FILTER_UPDATE(action) { - switch (action.payload.recordType) { - case RecordSetType.IAPP: - yield put(WhatsHere.iapp_rows_request()); - break; - default: - yield put(WhatsHere.activity_rows_request()); - break; +function* handle_WHATS_HERE_SORT_FILTER_UPDATE(record: PayloadAction>) { + const { recordType } = record.payload; + if (recordType === RecordSetType.IAPP) { + yield put(WhatsHere.iapp_rows_request()); + } else if (recordType === RecordSetType.Activity) { + yield put(WhatsHere.activity_rows_request()); } } function* handle_MAP_LABEL_EXTENT_FILTER_REQUEST(action) { const bbox = [action.payload.minX, action.payload.minY, action.payload.maxX, action.payload.maxY]; - const bounds = turf.bboxPolygon(bbox as any); + const bounds = bboxPolygon(bbox as any); yield put({ type: MAP_LABEL_EXTENT_FILTER_SUCCESS, @@ -546,7 +508,7 @@ function* handle_MAP_LABEL_EXTENT_FILTER_REQUEST(action) { function* handle_IAPP_EXTENT_FILTER_REQUEST(action) { const bbox = [action.payload.minX, action.payload.minY, action.payload.maxX, action.payload.maxY]; - const bounds = turf.bboxPolygon(bbox as any); + const bounds = bboxPolygon(bbox as any); yield put({ type: IAPP_EXTENT_FILTER_SUCCESS, @@ -804,9 +766,9 @@ function* handle_CUSTOM_LAYER_DRAWN(actions) { function* handle_MAP_ON_SHAPE_CREATE(action) { const callback = (width: number) => { - const newGeo = turf.buffer(action.payload.geometry, width / 10000); + const newGeo = buffer(action.payload.geometry, width / 10000) ?? action.payload; if (appModeUrl && /Activity/.test(appModeUrl) && !whatsHereToggle) { - return [{ type: ACTIVITY_UPDATE_GEO_REQUEST, payload: { geometry: [newGeo ? newGeo : action.payload] } }]; + return [{ type: ACTIVITY_UPDATE_GEO_REQUEST, payload: { geometry: [newGeo] } }]; } }; const appModeUrl = yield select((state: any) => state.AppMode.url); @@ -838,13 +800,12 @@ function* handle_MAP_ON_SHAPE_UPDATE(action) { } } -function* handle_MAP_TOGGLE_GEOJSON_CACHE(action) { +function handle_MAP_TOGGLE_GEOJSON_CACHE(action) { location.reload(); } -function* handle_WHATS_HERE_SERVER_FILTERED_IDS_FETCHED(action) { - yield put(WhatsHere.iapp_rows_request()); - yield put(WhatsHere.activity_rows_request()); +function* handle_WHATS_HERE_SERVER_FILTERED_IDS_FETCHED() { + yield all([put(WhatsHere.iapp_rows_request()), put(WhatsHere.activity_rows_request())]); } function* handle_RECORDSET_ROTATE_COLOUR(action: PayloadAction) { diff --git a/app/src/state/sagas/map/dataAccess.ts b/app/src/state/sagas/map/dataAccess.ts index b2aa53dee..67b2e07a2 100644 --- a/app/src/state/sagas/map/dataAccess.ts +++ b/app/src/state/sagas/map/dataAccess.ts @@ -120,7 +120,7 @@ export function* handle_ACTIVITIES_GET_IDS_FOR_RECORDSET_REQUEST(action) { type: ACTIVITIES_GET_IDS_FOR_RECORDSET_SUCCESS, payload: { recordSetID: action.payload.recordSetID, - IDList: recordSet.cachedMetadata.idList, + IDList: recordSet.cachedMetadata.idList ?? [], tableFiltersHash: action.payload.tableFiltersHash } }); diff --git a/app/src/utils/bboxToPolygon.ts b/app/src/utils/bboxToPolygon.ts new file mode 100644 index 000000000..689940a61 --- /dev/null +++ b/app/src/utils/bboxToPolygon.ts @@ -0,0 +1,28 @@ +import { Feature, Polygon } from '@turf/helpers'; + +/** + * @desc Takes bounding from recordset metadata and convert into a Geojson Polygon + * @param bbox Cached bounding box + * @returns {Feature} Geojson Bounding box + */ +function bboxToPolygon(bbox: Record): Feature { + const { minLatitude, minLongitude, maxLatitude, maxLongitude } = bbox; + return { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [minLongitude, minLatitude], + [minLongitude, maxLatitude], + [maxLongitude, maxLatitude], + [maxLongitude, minLatitude], + [minLongitude, minLatitude] + ] + ] + }, + properties: {} + }; +} + +export default bboxToPolygon; diff --git a/app/src/utils/getBoundingBoxFromRecordsetFilters.ts b/app/src/utils/getBoundingBoxFromRecordsetFilters.ts index 1094755d8..788b9a517 100644 --- a/app/src/utils/getBoundingBoxFromRecordsetFilters.ts +++ b/app/src/utils/getBoundingBoxFromRecordsetFilters.ts @@ -23,12 +23,12 @@ const getBoundingBoxFromRecordsetFilters = async (recordSet: UserRecordSet): Pro body: JSON.stringify({ filterObjects: [filterObj] }) }).then((data) => data.json()); - const [minX, minY, maxX, maxY] = bbox(parse(data.bbox)); + const [minLongitude, minLatitude, maxLongitude, maxLatitude] = bbox(parse(data.bbox)); return { - minLatitude: minY, - maxLatitude: maxY, - minLongitude: minX, - maxLongitude: maxX + minLatitude: minLatitude, + maxLatitude: maxLongitude, + minLongitude: minLongitude, + maxLongitude: maxLatitude }; }; diff --git a/app/src/utils/record-cache/localforage-cache.ts b/app/src/utils/record-cache/localforage-cache.ts index fb95c1d7e..33d793e1c 100644 --- a/app/src/utils/record-cache/localforage-cache.ts +++ b/app/src/utils/record-cache/localforage-cache.ts @@ -123,7 +123,7 @@ class LocalForageRecordCacheService extends RecordCacheService { const label = data.short_id; const features = data.geometry ?? []; features.forEach((feature: Feature) => { - feature.properties = { name: label }; + feature.properties = { name: label, description: id }; centroidArr.push(centroid(feature)); geoJsonArr.push(feature); });