@@ -323,6 +335,7 @@ export const Map = ({ children }) => {
+
{children}
diff --git a/app/src/UI/LegacyMap/helpers/components/CurrentActivityLayer.tsx b/app/src/UI/LegacyMap/helpers/components/CurrentActivityLayer.tsx
new file mode 100644
index 000000000..a3497e0e1
--- /dev/null
+++ b/app/src/UI/LegacyMap/helpers/components/CurrentActivityLayer.tsx
@@ -0,0 +1,99 @@
+import { useContext, useEffect, useState } from 'react';
+import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext';
+import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions';
+import { useSelector } from 'utils/use_selector';
+
+const CurrentActivityLayer = ({ mapReady }) => {
+ const map = useContext(MapContext);
+ const [geo, setGeo] = useState(null);
+
+ const activityGeometryArray = useSelector((state) => state.ActivityPage.activity?.geometry);
+ const { url } = useSelector((state) => state.AppMode);
+
+ // react to changes in the geometry or current page and set our rendered geo appropriately
+ // render if a) we're on the Activity page and b) There is a geo object in the Activity
+ useEffect(() => {
+ if (activityGeometryArray && activityGeometryArray[0] && url?.includes('Activity')) {
+ setGeo(activityGeometryArray[0]);
+ } else {
+ setGeo(null);
+ }
+ }, [activityGeometryArray, url]);
+
+ useEffect(() => {
+ if (!map) return;
+ if (!mapReady) return;
+
+ // add the layer if needed
+
+ const LAYER_ID = 'current-activity-';
+
+ const SHAPE_LAYER = `${LAYER_ID}-shape`;
+ const OUTLINE_LAYER = `${LAYER_ID}-outline`;
+ const ZOOM_CIRCLE_LAYER = `${LAYER_ID}-zoomoutcircle`;
+
+ if (geo) {
+ map
+ .addSource(LAYER_ID, {
+ type: 'geojson',
+ data: geo
+ })
+ .addLayer(
+ {
+ id: SHAPE_LAYER,
+ source: LAYER_ID,
+ type: 'fill',
+ paint: {
+ 'fill-color': 'white',
+ 'fill-outline-color': 'black',
+ 'fill-opacity': 0.7
+ },
+ minzoom: 0,
+ maxzoom: 24
+ },
+ LAYER_Z_FOREGROUND
+ )
+ .addLayer(
+ {
+ id: OUTLINE_LAYER,
+ source: LAYER_ID,
+ type: 'line',
+ paint: {
+ 'line-color': 'black',
+ 'line-opacity': 1,
+ 'line-width': 3
+ },
+ minzoom: 0,
+ maxzoom: 24
+ },
+ LAYER_Z_FOREGROUND
+ )
+ .addLayer(
+ {
+ id: ZOOM_CIRCLE_LAYER,
+ source: LAYER_ID,
+ type: 'circle',
+ paint: {
+ 'circle-color': 'white',
+ 'circle-radius': 3
+ },
+ minzoom: 0,
+ maxzoom: 24
+ },
+ LAYER_Z_FOREGROUND
+ );
+
+ return () => {
+ // cleanup effect -- remove created entries in reverse
+ map.removeLayer(ZOOM_CIRCLE_LAYER);
+ map.removeLayer(OUTLINE_LAYER);
+ map.removeLayer(SHAPE_LAYER);
+ map.removeSource(LAYER_ID);
+ };
+ }
+ }, [geo]);
+
+ return null;
+};
+
+export { CurrentActivityLayer };
diff --git a/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx b/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx
index 1196c807e..87e57a2b7 100644
--- a/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx
+++ b/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx
@@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext';
-import MapboxDraw, { DrawCustomMode } from '@mapbox/mapbox-gl-draw';
+import MapboxDraw from '@mapbox/mapbox-gl-draw';
import DrawRectangle from 'mapbox-gl-draw-rectangle-mode';
import { useDispatch, useSelector } from 'utils/use_selector';
import { MAP_ON_SHAPE_CREATE, MAP_ON_SHAPE_UPDATE } from 'state/actions';
@@ -34,7 +34,6 @@ const DrawControls = () => {
const whatsHereToggle = useSelector((state) => state.Map.whatsHere.toggle);
const tileCacheMode = useSelector((state) => state.Map.tileCacheMode);
- const activityGeo = useSelector((state) => state.ActivityPage.activity?.geometry);
const drawingCustomLayer = useSelector((state) => state.Map.drawingCustomLayer);
const appModeURL = useSelector((state) => state.AppMode.url);
@@ -113,7 +112,13 @@ const DrawControls = () => {
const drawShapeUpdate = useCallback((event) => {
if (!drawInstance.current) return;
+ if (!['direct_select', 'simple_select'].includes(drawInstance.current.getMode())) {
+ // we're not done drawing until we revert to one of these modes
+ return;
+ }
+
const editedGeo = drawInstance.current.getAll().features[0];
+
if (editedGeo?.id !== event?.features?.[0]?.id) {
dispatch({ type: MAP_ON_SHAPE_UPDATE, payload: editedGeo });
}
@@ -137,10 +142,6 @@ const DrawControls = () => {
break;
case TargetMode.ACTIVITY:
drawInstance.current.changeMode('do_nothing');
- if (activityGeo && activityGeo[0] && activityGeo[0].id) {
- drawInstance.current.deleteAll();
- drawInstance.current.add(activityGeo[0]);
- }
break;
default:
break;
@@ -148,38 +149,31 @@ const DrawControls = () => {
//drawInstance.current.changeMode('whats_here_box_mode');
}
- }, [mode, activityGeo]);
+ }, [mode]);
useEffect(() => {
if (!map) {
return;
}
- const modes = (() => {
- return Object.assign(
- {
- draw_rectangle: DrawRectangle,
- do_nothing: DoNothing,
- lots_of_points: LotsOfPointsMode,
- whats_here_box_mode: WhatsHereBoxMode
- },
- MapboxDraw.modes
- );
- })();
-
drawInstance.current = new MapboxDraw({
displayControlsDefault: true,
controls: {
combine_features: false,
uncombine_features: false
},
- defaultMode: 'simple_select',
- modes: modes as { [modeKey: string]: DrawCustomMode },
+ modes: {
+ ...MapboxDraw.modes,
+ draw_rectangle: DrawRectangle,
+ do_nothing: DoNothing,
+ lots_of_points: LotsOfPointsMode,
+ whats_here_box_mode: WhatsHereBoxMode
+ },
styles: [
{
id: 'gl-draw-line',
type: 'line',
- filter: ['all', ['==', '$type', 'LineString']],
+ // filter: ['all', ['==', '$type', 'LineString']],
layout: {
'line-cap': 'round',
'line-join': 'round'
@@ -187,7 +181,7 @@ const DrawControls = () => {
paint: {
'line-color': '#D20C0C',
'line-dasharray': [0.2, 2],
- 'line-width': 2
+ 'line-width': 3
}
}
]
diff --git a/app/src/UI/LegacyMap/helpers/functional/current-record.ts b/app/src/UI/LegacyMap/helpers/functional/current-record.ts
index 136233f40..163dc8260 100644
--- a/app/src/UI/LegacyMap/helpers/functional/current-record.ts
+++ b/app/src/UI/LegacyMap/helpers/functional/current-record.ts
@@ -24,40 +24,48 @@ export const refreshCurrentRecMakers = (map, options: any) => {
};
export const refreshHighlightedRecord = (map, options: any) => {
- const layerID = 'highlightRecordLayer';
- if (map && map.getLayer(layerID + 'shape')) {
- map.removeLayer(layerID + 'shape');
+ const LAYER_ID = 'highlightRecordLayer';
+
+ const SHAPE_LAYER = `${LAYER_ID}-shape`;
+ const OUTLINE_LAYER = `${LAYER_ID}-outline`;
+ const ZOOM_CIRCLE_LAYER = `${LAYER_ID}-zoomoutcircle`;
+
+ if (!map) {
+ return;
+ }
+
+ if (map.getLayer(SHAPE_LAYER)) {
+ map.removeLayer(SHAPE_LAYER);
}
- if (map && map.getLayer(layerID + 'outline')) {
- map.removeLayer(layerID + 'outline');
+ if (map.getLayer(OUTLINE_LAYER)) {
+ map.removeLayer(OUTLINE_LAYER);
}
- if (map && map.getLayer(layerID + 'zoomoutcircle')) {
- map.removeLayer(layerID + 'zoomoutcircle');
+ if (map.getLayer(ZOOM_CIRCLE_LAYER)) {
+ map.removeLayer(ZOOM_CIRCLE_LAYER);
}
- if (map && map.getLayer(layerID)) {
- map.removeLayer(layerID);
+ if (map.getLayer(LAYER_ID)) {
+ map.removeLayer(LAYER_ID);
}
- if (map && map.getSource(layerID)) {
- map.removeSource(layerID);
+ if (map.getSource(LAYER_ID)) {
+ map.removeSource(LAYER_ID);
}
if (
- map &&
options.userRecordOnHoverRecordType === 'Activity' &&
options.userRecordOnHoverRecordRow &&
options.userRecordOnHoverRecordRow?.geometry?.[0]
) {
map
- .addSource(layerID, {
+ .addSource(LAYER_ID, {
type: 'geojson',
data: options.userRecordOnHoverRecordRow.geometry[0]
})
.addLayer(
{
- id: layerID + 'shape',
- source: layerID,
+ id: SHAPE_LAYER,
+ source: LAYER_ID,
type: 'fill',
paint: {
'fill-color': 'white',
@@ -71,8 +79,8 @@ export const refreshHighlightedRecord = (map, options: any) => {
)
.addLayer(
{
- id: layerID + 'outline',
- source: layerID,
+ id: OUTLINE_LAYER,
+ source: LAYER_ID,
type: 'line',
paint: {
'line-color': 'black',
@@ -86,8 +94,8 @@ export const refreshHighlightedRecord = (map, options: any) => {
)
.addLayer(
{
- id: layerID + 'zoomoutcircle',
- source: layerID,
+ id: ZOOM_CIRCLE_LAYER,
+ source: LAYER_ID,
type: 'circle',
paint: {
'circle-color': 'white',
@@ -100,16 +108,16 @@ export const refreshHighlightedRecord = (map, options: any) => {
);
}
- if (map && options.userRecordOnHoverRecordType === 'IAPP' && options.userRecordOnHoverRecordRow) {
+ if (options.userRecordOnHoverRecordType === 'IAPP' && options.userRecordOnHoverRecordRow) {
map
- .addSource(layerID, {
+ .addSource(LAYER_ID, {
type: 'geojson',
data: options.userRecordOnHoverRecordRow.geometry
})
.addLayer(
{
- id: layerID,
- source: layerID,
+ id: LAYER_ID,
+ source: LAYER_ID,
type: 'circle',
paint: {
'circle-color': 'yellow',
diff --git a/app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts b/app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts
index b617291db..a24f3b7ef 100644
--- a/app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts
+++ b/app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts
@@ -10,23 +10,33 @@ import { LAYER_Z_BACKGROUND, LAYER_Z_FOREGROUND, LAYER_Z_MID } from 'UI/LegacyMa
import { FALLBACK_COLOR } from 'UI/LegacyMap/helpers/functional/constants';
import { safelySetPaintProperty } from 'UI/LegacyMap/helpers/functional/utility-functions';
import { MOBILE } from 'state/build-time-config';
-import { RecordSetType } from 'interfaces/UserRecordSet';
+import { RecordSetType, UserRecordCacheStatus } from 'interfaces/UserRecordSet';
import VECTOR_MAP_FONT_FACE from 'constants/vectorMapFontFace';
+import { RecordCacheServiceFactory } from 'utils/record-cache/context';
const LAYER_ID_PREFIX = 'recordset-layer-';
/** DRY Handler for formatting LayerIDs */
const formatLayerID = (recordSetID: string, tableFiltersHash: string): string =>
`${LAYER_ID_PREFIX}${recordSetID}-hash-${tableFiltersHash}`;
-export const createOfflineIappLayer = (map: maplibregl.Map, layer: any) => {
- if (!layer?.layerState?.cacheMetadata?.hasOwnProperty('cachedGeoJson')) {
+export const createOfflineIappLayer = async (map: maplibregl.Map, layer: any) => {
+ if (layer?.layerState?.cacheMetadataStatus !== UserRecordCacheStatus.CACHED || !layer.layerState.mapToggle) {
+ return;
+ }
+ const service = await RecordCacheServiceFactory.getPlatformInstance();
+ const repo = await service.getRepository(layer.recordSetID);
+
+ if (!repo?.cachedGeoJson) {
return;
}
const layerID = formatLayerID(layer.recordSetID, layer.tableFiltersHash);
- const source: GeoJSONSourceSpecification = layer.layerState.cacheMetadata.cachedGeoJson;
+ const source: GeoJSONSourceSpecification = repo.cachedGeoJson;
const color: string = layer.layerState.color ?? FALLBACK_COLOR;
const labelLayer: SymbolLayerSpecification = getLabelLayer(layerID, { color, minzoom: 10, get_tag: 'name' });
const circleLayer: CircleLayerSpecification = getCircleMarkerZoomedOutLayer(layerID, { color });
+
+ const existingSource = map.getSource(layerID);
+ if (existingSource) return; // Due to the async nature of the local DB Calls, check the layer wasn't created during a re-render
map.addSource(layerID, source);
map.addLayer(circleLayer, LAYER_Z_FOREGROUND);
map.addLayer(labelLayer, LAYER_Z_FOREGROUND);
@@ -177,20 +187,24 @@ const getLabelLayer = (layerID: string, options: LayerOptions): SymbolLayerSpeci
* @desc Uses the device's recordset Cache data to display geoJson Layers on the map when offline
* Displays two layers: Points at high levels, and shapes at lower
*/
-export const createOfflineActivityLayer = (map: maplibregl.Map, layer: any) => {
- if (
- (['1', '2'].includes(layer.recordSetID) && !layer.layerState.colorScheme) ||
- !layer?.layerState?.cacheMetadata?.hasOwnProperty('cachedGeoJson')
- ) {
+export const createOfflineActivityLayer = async (map: maplibregl.Map, layer: any) => {
+ if (layer?.layerState?.cacheMetadataStatus !== UserRecordCacheStatus.CACHED || !layer.layerState.mapToggle) {
+ return;
+ }
+ const service = await RecordCacheServiceFactory.getPlatformInstance();
+ const metadata = await service.getRepository(layer.recordSetID);
+
+ if (!metadata?.cachedCentroid || !metadata?.cachedGeoJson) {
return;
}
+
const CENTROID_TO_GEOJSON_ZOOM = 12;
const GEOJSON_ID = formatLayerID(layer.recordSetID, layer.tableFiltersHash);
const CENTROID_ID = `${GEOJSON_ID}-centroid`;
const color = getPaintBySchemeOrColor(layer);
- const geoJsonSourcObj: GeoJSONSourceSpecification = layer.layerState.cacheMetadata.cachedGeoJson;
- const centroidSourceObj: GeoJSONSourceSpecification = layer.layerState.cacheMetadata.cachedCentroid;
+ const geoJsonSourceObj: GeoJSONSourceSpecification = metadata.cachedGeoJson;
+ const centroidSourceObj: GeoJSONSourceSpecification = metadata.cachedCentroid;
const circleMarkerZoomedOutLayerCentroid: CircleLayerSpecification = getCircleMarkerZoomedOutLayer(CENTROID_ID, {
color,
@@ -214,7 +228,9 @@ export const createOfflineActivityLayer = (map: maplibregl.Map, layer: any) => {
get_tag: 'name'
});
- map.addSource(GEOJSON_ID, geoJsonSourcObj);
+ const existingSource = map.getSource(GEOJSON_ID);
+ if (existingSource) return; // Due to the async nature of the local DB Calls, check the layer wasn't created during a re-render
+ map.addSource(GEOJSON_ID, geoJsonSourceObj);
map.addLayer(fillLayer, LAYER_Z_FOREGROUND);
map.addLayer(borderLayer, LAYER_Z_FOREGROUND);
map.addLayer(circleMarkerZoomedOutLayer, LAYER_Z_FOREGROUND);
@@ -375,13 +391,13 @@ export const rebuildLayersOnTableHashUpdate = (
});
// now update the layers that are in the store
- storeLayers.map((layer: Record
) => {
+ storeLayers.forEach(async (layer: Record) => {
if ((layer.geoJSON && layer.loading === false) || (mode === 'VECTOR_ENDPOINT' && layer.filterObject)) {
const sourceId = formatLayerID(layer.recordSetID, layer.tableFiltersHash);
deleteStaleRecordsetLayer(map, layer);
const existingSource = map.getSource(sourceId);
if (existingSource) return;
- createMapLayer(map, layer, mode, API_BASE, MOBILE_OFFLINE);
+ await createMapLayer(map, layer, mode, API_BASE, MOBILE_OFFLINE);
}
});
};
@@ -389,22 +405,22 @@ export const rebuildLayersOnTableHashUpdate = (
/**
* @desc Handler logic for creating a new layer based on Network condition and recordset type
*/
-const createMapLayer = (
+const createMapLayer = async (
map: maplibregl.Map,
layer: Record,
mapMode: string,
apiBase: string,
isOfflineLayer: boolean
-): void => {
+): Promise => {
if (layer.type === RecordSetType.Activity) {
if (isOfflineLayer) {
- createOfflineActivityLayer(map, layer);
+ await createOfflineActivityLayer(map, layer);
} else {
createOnlineActivityLayer(map, layer, mapMode, apiBase);
}
} else if (layer.type === RecordSetType.IAPP) {
if (isOfflineLayer) {
- createOfflineIappLayer(map, layer);
+ await createOfflineIappLayer(map, layer);
} else {
createOnlineIappLayer(map, layer, mapMode, apiBase);
}
@@ -470,20 +486,3 @@ export const refreshVisibilityOnToggleUpdate = (storeLayers, map: maplibregl.Map
});
});
};
-
-/** TODO: */
-export const removeDeletedRecordSetLayersOnRecordSetDelete = (storeLayers, map) => {
- map.getLayersOrder().map((layer: any) => {
- if (
- storeLayers.filter((l: any) => l.recordSetID === layer).length === 0 &&
- !['wms-test-layer', 'wms-test-layer2', 'invasives-vector', 'buildings'].includes(layer)
- ) {
- //map.current.removeLayer(layer);
- //map.current.removeSource(layer);
- }
- });
- storeLayers.map((layer) => {
- // get matching layers for type
- // update visibility if doesn't match
- });
-};
diff --git a/app/src/UI/Overlay/Records/RecordSet/Filter.tsx b/app/src/UI/Overlay/Records/RecordSet/Filter.tsx
index bb4f8af8f..2d7a65f89 100644
--- a/app/src/UI/Overlay/Records/RecordSet/Filter.tsx
+++ b/app/src/UI/Overlay/Records/RecordSet/Filter.tsx
@@ -291,14 +291,16 @@ const Filter = ({ setID, id, userOfflineMobile }: PropTypes) => {
{input}
- removeFilter('tableFilter')}
- variant="contained"
- >
- Delete
-
+
+ removeFilter('tableFilter')}
+ variant="contained"
+ >
+ Delete
+
+
diff --git a/app/src/UI/Overlay/Records/RecordSet/RecordSet.tsx b/app/src/UI/Overlay/Records/RecordSet/RecordSet.tsx
index 0aed27aef..6cd89b32e 100644
--- a/app/src/UI/Overlay/Records/RecordSet/RecordSet.tsx
+++ b/app/src/UI/Overlay/Records/RecordSet/RecordSet.tsx
@@ -58,53 +58,57 @@ export const RecordSet = (props) => {
- {
- dispatch({
- type: RECORDSET_CLEAR_FILTERS,
- payload: {
- setID: props.setID
- }
- });
- }}
- variant="contained"
- >
- Clear Filters
-
-
+
+ {
+ dispatch({
+ type: RECORDSET_CLEAR_FILTERS,
+ payload: {
+ setID: props.setID
+ }
+ });
+ }}
+ variant="contained"
+ >
+ Clear Filters
+
+
+
- {
- dispatch({
- type: RECORDSETS_TOGGLE_VIEW_FILTER
- });
- }}
- variant="contained"
- >
- {viewFilters ? (
- <>
- Hide Filters
-
-
- >
- ) : (
- <>
- Show Filters{' '}
- {(recordSet?.tableFilters?.length || 0) > 0 &&
- !onlyFilterIsForDrafts &&
- `(${recordSet?.tableFilters?.length})`}
-
-
- >
- )}
-
+
+ {
+ dispatch({
+ type: RECORDSETS_TOGGLE_VIEW_FILTER
+ });
+ }}
+ variant="contained"
+ >
+ {viewFilters ? (
+ <>
+ Hide Filters
+
+
+ >
+ ) : (
+ <>
+ Show Filters{' '}
+ {(recordSet?.tableFilters?.length || 0) > 0 &&
+ !onlyFilterIsForDrafts &&
+ `(${recordSet?.tableFilters?.length})`}
+
+
+ >
+ )}
+
+
@@ -112,27 +116,29 @@ export const RecordSet = (props) => {
classes={{ tooltip: 'toolTip' }}
title="Add a new filter, drawn, uploaded KML, or just text search on a field."
>
- {
- dispatch({
- type: RECORDSET_ADD_FILTER,
- payload: {
- filterType: 'tableFilter',
- // short id if activity record set otherwise site_ID
- field: tableType === 'Activity' ? 'short_id' : 'site_id',
- setID: props.setID,
- operator: 'CONTAINS',
- operator2: 'AND',
- blockFetchForNow: true
- }
- });
- }}
- variant="contained"
- >
- Add Filter +
-
+
+ {
+ dispatch({
+ type: RECORDSET_ADD_FILTER,
+ payload: {
+ filterType: 'tableFilter',
+ // short id if activity record set otherwise site_ID
+ field: tableType === 'Activity' ? 'short_id' : 'site_id',
+ setID: props.setID,
+ operator: 'CONTAINS',
+ operator2: 'AND',
+ blockFetchForNow: true
+ }
+ });
+ }}
+ variant="contained"
+ >
+ Add Filter +
+
+
diff --git a/app/src/UI/Overlay/Records/RecordSetCacheButtons.tsx b/app/src/UI/Overlay/Records/RecordSetCacheButtons.tsx
index d1ca507cf..889a35529 100644
--- a/app/src/UI/Overlay/Records/RecordSetCacheButtons.tsx
+++ b/app/src/UI/Overlay/Records/RecordSetCacheButtons.tsx
@@ -18,7 +18,7 @@ const RecordSetCacheButtons = ({ recordSet, setId }: PropTypes) => {
const handleClick = (e: MouseEvent) => {
e.stopPropagation();
- switch (recordSet.cacheMetadata.status) {
+ switch (recordSet.cacheMetadataStatus) {
case UserRecordCacheStatus.NOT_CACHED:
downloadCache();
break;
@@ -96,9 +96,9 @@ const RecordSetCacheButtons = ({ recordSet, setId }: PropTypes) => {
UserRecordCacheStatus.NOT_CACHED,
UserRecordCacheStatus.ERROR,
UserRecordCacheStatus.DOWNLOADING
- ].includes(recordSet.cacheMetadata?.status)
+ ].includes(recordSet.cacheMetadataStatus)
);
- }, [recordSet.cacheMetadata?.status, connected]);
+ }, [recordSet.cacheMetadataStatus, connected]);
return (
@@ -109,7 +109,7 @@ const RecordSetCacheButtons = ({ recordSet, setId }: PropTypes) => {
onClick={handleClick}
variant="outlined"
>
- {formatStatusKey(recordSet.cacheMetadata?.status)}
+ {formatStatusKey(recordSet.cacheMetadataStatus)}
diff --git a/app/src/interfaces/UserRecordSet.ts b/app/src/interfaces/UserRecordSet.ts
index 81e465daf..0bed9341a 100644
--- a/app/src/interfaces/UserRecordSet.ts
+++ b/app/src/interfaces/UserRecordSet.ts
@@ -1,6 +1,3 @@
-import { GeoJSONSourceSpecification } from 'maplibre-gl';
-import { RepositoryBoundingBoxSpec } from 'utils/tile-cache';
-
export enum RecordSetType {
IAPP = 'IAPP',
Activity = 'Activity'
@@ -32,11 +29,5 @@ export interface UserRecordSet {
name: string;
server_id: any;
};
- cacheMetadata: {
- status: UserRecordCacheStatus;
- idList?: string[];
- cachedGeoJson?: GeoJSONSourceSpecification;
- cachedCentroid?: GeoJSONSourceSpecification;
- bbox?: RepositoryBoundingBoxSpec;
- };
+ cacheMetadataStatus: UserRecordCacheStatus;
}
diff --git a/app/src/main.tsx b/app/src/main.tsx
index 5eeaa8a49..4e51e5180 100644
--- a/app/src/main.tsx
+++ b/app/src/main.tsx
@@ -10,6 +10,7 @@ import { TileCacheService } from 'utils/tile-cache';
import { Context, TileCacheServiceFactory } from 'utils/tile-cache/context';
import { MOBILE } from 'state/build-time-config';
import TileCache from 'state/actions/cache/TileCache';
+import { RecordCacheServiceFactory } from 'utils/record-cache/context';
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(import.meta.env.MODE === 'production' ? '/worker.js' : '/dev-sw.js?dev-sw', {
@@ -21,8 +22,10 @@ async function mountApp(CONFIG) {
const { store, persistor } = setupStore(CONFIG);
let tileCache: TileCacheService | null = null;
+
if (MOBILE) {
tileCache = await TileCacheServiceFactory.getPlatformInstance();
+ await RecordCacheServiceFactory.getPlatformInstance();
// load any caches present
store.dispatch(TileCache.repositoryList());
}
diff --git a/app/src/state/actions/cache/RecordCache.ts b/app/src/state/actions/cache/RecordCache.ts
index 80758610c..41022046e 100644
--- a/app/src/state/actions/cache/RecordCache.ts
+++ b/app/src/state/actions/cache/RecordCache.ts
@@ -30,15 +30,14 @@ class RecordCache {
) => {
const service = await RecordCacheServiceFactory.getPlatformInstance();
const state: RootState = getState() as RootState;
-
- const idsToCache: string[] = state.Map.layers
- .find((l) => l.recordSetID == spec.setId)
- .IDList.map((id: string | number) => id.toString());
+ const idsToCache: string[] =
+ state.Map.layers.find((l) => l.recordSetID == spec.setId)?.IDList.map((id: string | number) => id.toString()) ??
+ [];
const recordSet = state.UserSettings.recordSets[spec.setId];
const bbox = await getBoundingBoxFromRecordsetFilters(recordSet);
- const responseData = await service.downloadCache({
+ const downloadCompleted = await service.download({
API_BASE: state.Configuration.current.API_BASE,
bbox,
idsToCache,
@@ -46,14 +45,9 @@ class RecordCache {
setId: spec.setId
});
- // Will Refactor the current uses of Cache Metadata separately [Maintain only cache status]
return {
- status: UserRecordCacheStatus.CACHED,
- idList: idsToCache,
- bbox: bbox,
setId: spec.setId,
- cachedGeoJson: responseData.cachedGeoJson,
- cachedCentroid: responseData.cachedCentroid
+ status: downloadCompleted ? UserRecordCacheStatus.CACHED : UserRecordCacheStatus.NOT_CACHED
};
}
);
diff --git a/app/src/state/actions/userSettings/RecordSet.ts b/app/src/state/actions/userSettings/RecordSet.ts
index 956a49e33..563d1eeff 100644
--- a/app/src/state/actions/userSettings/RecordSet.ts
+++ b/app/src/state/actions/userSettings/RecordSet.ts
@@ -67,7 +67,7 @@ class RecordSet {
if (
MOBILE &&
[UserRecordCacheStatus.CACHED, UserRecordCacheStatus.DOWNLOADING].includes(
- recordSets[spec.setId]?.cacheMetadata?.status
+ recordSets[spec.setId].cacheMetadataStatus
)
) {
const deletionResult = await thunkAPI.dispatch(RecordCache.deleteCache(spec));
@@ -99,9 +99,7 @@ class RecordSet {
name: '',
server_id: 0
},
- cacheMetadata: {
- status: UserRecordCacheStatus.NOT_CACHED
- }
+ cacheMetadataStatus: UserRecordCacheStatus.NOT_CACHED
});
}
diff --git a/app/src/state/reducers/alertsAndPrompts.ts b/app/src/state/reducers/alertsAndPrompts.ts
index 30f2cbdf2..04c8ff8e7 100644
--- a/app/src/state/reducers/alertsAndPrompts.ts
+++ b/app/src/state/reducers/alertsAndPrompts.ts
@@ -6,6 +6,7 @@ import Prompt from 'state/actions/prompts/Prompt';
import { PromptAction } from 'interfaces/prompt-interfaces';
import RecordCache from 'state/actions/cache/RecordCache';
import cacheAlertMessages from 'constants/alerts/cacheAlerts';
+import { UserRecordCacheStatus } from 'interfaces/UserRecordSet';
interface AlertsAndPromptsState {
alerts: AlertMessage[];
@@ -50,12 +51,13 @@ export function createAlertsAndPromptsReducer(
} else if (RegExp(Prompt.NEW_PROMPT).exec(action.type)) {
const newPrompt: PromptAction = action.payload;
draftState.prompts = addPrompt(state.prompts, newPrompt);
- } else if (RecordCache.requestCaching.fulfilled.match(action)) {
+ } else if (
+ RecordCache.requestCaching.fulfilled.match(action) &&
+ action.payload.status === UserRecordCacheStatus.CACHED
+ ) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetCacheSuccess);
} else if (RecordCache.requestCaching.rejected.match(action)) {
- if (action?.error?.message !== 'Early Exit') {
- draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetCacheFailed);
- }
+ draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetCacheFailed);
} else if (RecordCache.deleteCache.rejected.match(action)) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetDeleteCacheFailed);
} else if (RecordCache.stopDownload.fulfilled.match(action)) {
diff --git a/app/src/state/reducers/map.ts b/app/src/state/reducers/map.ts
index 4fbf687cc..481d13209 100644
--- a/app/src/state/reducers/map.ts
+++ b/app/src/state/reducers/map.ts
@@ -60,9 +60,9 @@ import { SortFilter } from 'interfaces/filterParams';
import TileCache from 'state/actions/cache/TileCache';
import MapActions from 'state/actions/map';
import GeoTracking from 'state/actions/geotracking/GeoTracking';
-import RecordCache from 'state/actions/cache/RecordCache';
import IappActions from 'state/actions/activity/Iapp';
import Activity from 'state/actions/activity/Activity';
+import RecordCache from 'state/actions/cache/RecordCache';
export enum LeafletWhosEditingEnum {
ACTIVITY = 'ACTIVITY',
@@ -417,9 +417,11 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma
in) and builder.addCase instead of switches, although I assume you lose fallthrough cases then.
*/
return createNextState(state, (draftState: Draft) => {
- if (UserSettings.RecordSet.remove.match(action)) {
- const index = draftState.layers.findIndex((layer) => layer.recordSetID === action.payload);
- draftState.layers.splice(index, 1);
+ if (UserSettings.RecordSet.requestRemoval.fulfilled.match(action)) {
+ const index = draftState.layers.findIndex((layer) => layer.recordSetID === action.meta.arg.setId);
+ if (index !== -1) {
+ draftState.layers.splice(index, 1);
+ }
} else if (UserSettings.RecordSet.set.match(action)) {
const layerIndex = draftState.layers.findIndex((layer) => layer.recordSetID === action.payload.setName);
Object.keys(action.payload.updatedSet).forEach((key) => {
@@ -471,14 +473,6 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma
for (const r of removalList) {
draftState.enabledOverlayLayers.splice(draftState.enabledOverlayLayers.indexOf(r), 1);
}
- } else if (RecordCache.requestCaching.fulfilled.match(action)) {
- const index = draftState.layers.findIndex((layer) => layer.recordSetID === action.payload.setId);
- draftState.layers[index].layerState.cacheMetadata = {
- ...draftState.layers[index].layerState.cacheMetadata,
- cachedCentroid: action.payload.cachedCentroid,
- cachedGeoJson: action.payload.cachedGeoJson,
- bbox: action.payload.bbox
- };
} else if (UserSettings.InitState.getSuccess.match(action)) {
Object.keys(action.payload.recordSets).forEach((setID) => {
let layerIndex = draftState.layers.findIndex((layer) => layer.recordSetID === setID);
@@ -517,24 +511,28 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma
page: 0
});
} else if (WhatsHere.server_filtered_ids_fetched.match(action)) {
- draftState.whatsHere.serverActivityIDs = action.payload.activities;
- draftState.whatsHere.serverIAPPIDs = action.payload.iapp;
+ const { iapp, activities } = action.payload;
+ draftState.whatsHere.serverActivityIDs = activities;
+ draftState.whatsHere.serverIAPPIDs = iapp;
const toggledOnActivityLayers = draftState.layers.filter(
({ type, layerState }) => type === RecordSetType.Activity && layerState.mapToggle
);
const toggledOnIAPPLayers = draftState.layers.filter(
({ 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?.layerState?.cacheMetadata?.idList ?? []
- );
- const iappIds = localIappIds.filter((l) => draftState.whatsHere.serverIAPPIDs.includes(l));
- const activityIds = localActivityIDs.filter((l) => draftState.whatsHere.serverActivityIDs.includes(l));
+
+ const localActivityIDs = toggledOnActivityLayers.flatMap((layer) => layer.IDList ?? []);
+ const localIappIds = toggledOnIAPPLayers.flatMap((layer) => layer.IDList ?? []);
+
+ const iappIds = localIappIds.filter((l) => iapp.includes(l) || iapp.includes(l.toString()));
+ const activityIds = localActivityIDs.filter((l) => activities.includes(l));
draftState.whatsHere.ActivityIDs = Array.from(new Set(activityIds));
draftState.whatsHere.IAPPIDs = Array.from(new Set(iappIds));
+ } else if (RecordCache.requestCaching.fulfilled.match(action)) {
+ const index = draftState.layers.findIndex((layer) => layer.recordSetID === action.meta.arg.setId);
+ if (index !== -1) {
+ draftState.layers[index].layerState.cacheMetadataStatus = action.payload.status;
+ }
} else if (WhatsHere.sort_filter_update.match(action)) {
const { field, direction } = action.payload;
if (action.payload.type === RecordSetType.IAPP) {
@@ -756,7 +754,7 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma
case IAPP_GET_IDS_FOR_RECORDSET_REQUEST: {
let index = draftState.layers.findIndex((layer) => layer.recordSetID === action.payload.recordSetID);
if (!draftState.layers[index]) {
- draftState.layers.push({ recordSetID: action.payload.recordSetID, type: RecordSetType.Activity });
+ draftState.layers.push({ recordSetID: action.payload.recordSetID, type: RecordSetType.IAPP });
index = draftState.layers.findIndex((layer) => layer.recordSetID === action.payload.recordSetID);
}
draftState.layers[index].tableFiltersHash = action.payload.tableFiltersHash;
@@ -812,8 +810,7 @@ function createMapReducer(configuration: AppConfig): (MapState, AnyAction) => Ma
if (!draftState.layers[index]) {
draftState.layers.push({
recordSetID: action.payload.recordSetID,
- type: action.payload.recordSetType,
- cacheMetadata: action.payload.cacheMetadata
+ type: action.payload.recordSetType
});
}
index = draftState.layers.findIndex((layer) => layer.recordSetID === action.payload.recordSetID);
diff --git a/app/src/state/reducers/userSettings.ts b/app/src/state/reducers/userSettings.ts
index 121e8c5ba..55e45e211 100644
--- a/app/src/state/reducers/userSettings.ts
+++ b/app/src/state/reducers/userSettings.ts
@@ -197,33 +197,15 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState
} else if (WhatsHere.toggle.match(action)) {
draftState.recordsExpanded = action.payload ? false : draftState.recordsExpanded;
} else if (RecordCache.requestCaching.pending.match(action)) {
- draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
- status: UserRecordCacheStatus.DOWNLOADING
- };
+ draftState.recordSets[action.meta.arg.setId].cacheMetadataStatus = UserRecordCacheStatus.DOWNLOADING;
} else if (RecordCache.requestCaching.rejected.match(action) || RecordCache.deleteCache.rejected.match(action)) {
- if (action.error.message === 'Early Exit') {
- draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
- status: UserRecordCacheStatus.NOT_CACHED
- };
- } else {
- draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
- status: UserRecordCacheStatus.ERROR
- };
- }
+ draftState.recordSets[action.meta.arg.setId].cacheMetadataStatus = UserRecordCacheStatus.ERROR;
} else if (RecordCache.requestCaching.fulfilled.match(action)) {
- draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
- status: UserRecordCacheStatus.CACHED,
- idList: action.payload.idList,
- bbox: action.payload.bbox,
- cachedGeoJson: action.payload.cachedGeoJson,
- cachedCentroid: action.payload.cachedCentroid
- };
+ draftState.recordSets[action.meta.arg.setId].cacheMetadataStatus = action.payload.status;
} else if (RecordCache.deleteCache.pending.match(action) || RecordCache.stopDownload.pending.match(action)) {
- draftState.recordSets[action.meta.arg.setId].cacheMetadata.status = UserRecordCacheStatus.DELETING;
+ draftState.recordSets[action.meta.arg.setId].cacheMetadataStatus = UserRecordCacheStatus.DELETING;
} else if (RecordCache.deleteCache.fulfilled.match(action)) {
- draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
- status: UserRecordCacheStatus.NOT_CACHED
- };
+ draftState.recordSets[action.meta.arg.setId].cacheMetadataStatus = UserRecordCacheStatus.NOT_CACHED;
} else if (Activity.deleteSuccess.match(action)) {
draftState.activeActivity = null;
draftState.activeActivityDescription = null;
diff --git a/app/src/state/sagas/activity/offline.ts b/app/src/state/sagas/activity/offline.ts
index 695f49d33..12550b135 100644
--- a/app/src/state/sagas/activity/offline.ts
+++ b/app/src/state/sagas/activity/offline.ts
@@ -74,7 +74,7 @@ export function* handle_ACTIVITY_GET_LOCAL_REQUEST(action: PayloadAction
}
} else {
try {
- const service: RecordCacheService = yield RecordCacheServiceFactory.getPlatformInstance();
+ const service = yield RecordCacheServiceFactory.getPlatformInstance();
const result = yield service.loadActivity(activityID);
const datav2 = {
diff --git a/app/src/state/sagas/iappsite/dataAccess.ts b/app/src/state/sagas/iappsite/dataAccess.ts
index 1ef408a19..561dc0ccb 100644
--- a/app/src/state/sagas/iappsite/dataAccess.ts
+++ b/app/src/state/sagas/iappsite/dataAccess.ts
@@ -15,7 +15,7 @@ export function* handle_IAPP_GET_REQUEST(iappId: PayloadAction) {
try {
const connected = yield select(selectNetworkConnected);
if (MOBILE && !connected) {
- const service: RecordCacheService = yield RecordCacheServiceFactory.getPlatformInstance();
+ const service = yield RecordCacheServiceFactory.getPlatformInstance();
const result = yield service.loadIapp(iappId.payload, IappRecordMode.Record);
yield put(IappActions.getSuccess(result));
} else {
diff --git a/app/src/state/sagas/map.ts b/app/src/state/sagas/map.ts
index 8374c6bf4..f08280b77 100644
--- a/app/src/state/sagas/map.ts
+++ b/app/src/state/sagas/map.ts
@@ -68,7 +68,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, UserRecordCacheStatus, UserRecordSet } from 'interfaces/UserRecordSet';
+import { RecordSetType, UserRecordCacheStatus } from 'interfaces/UserRecordSet';
import UserSettings from 'state/actions/userSettings/UserSettings';
import { SortFilter } from 'interfaces/filterParams';
import Activity from 'state/actions/activity/Activity';
@@ -84,6 +84,8 @@ import { RecordCacheServiceFactory } from 'utils/record-cache/context';
import bboxToPolygon from 'utils/bboxToPolygon';
import IappActions from 'state/actions/activity/Iapp';
import IappRecord from 'interfaces/IappRecord';
+import { RepositoryMetadata } from 'utils/record-cache';
+import NetworkActions from 'state/actions/network/NetworkActions';
function* handle_USER_SETTINGS_GET_INITIAL_STATE_SUCCESS(action) {
yield put({ type: MAP_INIT_REQUEST, payload: {} });
@@ -158,15 +160,21 @@ function* handle_WHATS_HERE_FEATURE(whatsHereFeature: PayloadAction) {
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 service = yield RecordCacheServiceFactory.getPlatformInstance();
+ const repos = yield service.listRepositories();
+
+ const recordSetsInBoundingBox = repos.filter((repo: RepositoryMetadata) => {
+ const { status, bbox } = repo;
+ return (
+ status === UserRecordCacheStatus.CACHED &&
+ bbox &&
+ booleanIntersects(whatsHereFeature.payload, bboxToPolygon(bbox as any))
+ );
});
+
const overlappingRecords: string[] = [];
recordSetsInBoundingBox.flatMap((set) =>
- recordSets[set].cacheMetadata.cachedGeoJson.data.features.forEach((shape: Feature) => {
+ set.cachedGeoJson.data.features.forEach((shape: Feature) => {
if (booleanIntersects(whatsHereFeature.payload, shape)) {
overlappingRecords.push(shape?.properties?.description);
}
@@ -215,7 +223,7 @@ function* handle_WHATS_HERE_IAPP_ROWS_REQUEST() {
let records: IappRecord[];
if (MOBILE && !connected) {
const service = yield RecordCacheServiceFactory.getPlatformInstance();
- records = yield service.fetchPaginatedCachedIappRecords(
+ records = yield service.getPaginatedCachedIappRecords(
whatsHere.IAPPIDs.map((id) => id.toString()),
whatsHere.IAPPPage,
whatsHere.IAPPLimit
@@ -305,7 +313,7 @@ function* handle_WHATS_HERE_ACTIVITY_ROWS_REQUEST() {
let records: UserRecord[];
if (MOBILE && !connected) {
const service = yield RecordCacheServiceFactory.getPlatformInstance();
- records = yield service.fetchPaginatedCachedRecords(
+ records = yield service.getPaginatedCachedActivityRecords(
whatsHere.ActivityIDs,
whatsHere.ActivityPage,
whatsHere.ActivityLimit
@@ -631,7 +639,7 @@ function* handle_PAGE_OR_LIMIT_UPDATE(action) {
}
}
-function* handle_MAP_INIT_FOR_RECORDSETS(action: PayloadAction) {
+function* handle_MAP_INIT_FOR_RECORDSETS() {
interface ActionType {
type: string;
payload: any;
@@ -657,7 +665,6 @@ function* handle_MAP_INIT_FOR_RECORDSETS(action: PayloadAction) {
const newUninitializedLayers = newLayerIDs.map((layer) => {
return { recordSetID: layer, recordSetType: userSettingsState.recordSets[layer].recordSetType };
});
-
// combined:
const allUninitializedLayers = [...currentUninitializedLayers, ...newUninitializedLayers];
@@ -724,14 +731,14 @@ function* handle_REMOVE_SERVER_BOUNDARY(action) {
yield put(UserSettings.KML.delete(action.payload.id));
}
-function* handle_DRAW_CUSTOM_LAYER(action) {
+function* handle_DRAW_CUSTOM_LAYER() {
const panelState = yield select((state) => state.AppMode.panelOpen);
if (panelState) {
yield put({ type: TOGGLE_PANEL });
}
}
-function* handle_CUSTOM_LAYER_DRAWN(actions) {
+function* handle_CUSTOM_LAYER_DRAWN() {
const panelState = yield select((state) => state.AppMode.panelOpen);
if (!panelState) {
yield put({ type: TOGGLE_PANEL });
@@ -774,7 +781,7 @@ function* handle_MAP_ON_SHAPE_UPDATE(action) {
}
}
-function handle_MAP_TOGGLE_GEOJSON_CACHE(action) {
+function handle_MAP_TOGGLE_GEOJSON_CACHE() {
location.reload();
}
@@ -829,10 +836,13 @@ function* activitiesPageSaga() {
takeEvery(DRAW_CUSTOM_LAYER, handle_DRAW_CUSTOM_LAYER),
takeEvery(CUSTOM_LAYER_DRAWN, handle_CUSTOM_LAYER_DRAWN),
+ //Conditions where we may want to redraw the Map layers, fetch IDLists, so on
+ takeEvery(NetworkActions.online, handle_MAP_INIT_FOR_RECORDSETS),
+ takeEvery(UserSettings.RecordSet.add, handle_MAP_INIT_FOR_RECORDSETS),
+ takeEvery(MAP_INIT_FOR_RECORDSET, handle_MAP_INIT_FOR_RECORDSETS),
+
takeEvery(REFETCH_SERVER_BOUNDARIES, refetchServerBoundaries),
takeEvery(WhatsHere.server_filtered_ids_fetched, handle_WHATS_HERE_SERVER_FILTERED_IDS_FETCHED),
-
- takeEvery(UserSettings.RecordSet.add, handle_MAP_INIT_FOR_RECORDSETS),
takeEvery(UserSettings.RecordSet.cycleColourById, handle_RECORDSET_ROTATE_COLOUR),
takeEvery(UserSettings.RecordSet.toggleVisibility, handle_RECORDSET_TOGGLE_VISIBILITY),
takeEvery(UserSettings.RecordSet.toggleLabelVisibility, handle_RECORDSET_TOGGLE_LABEL_VISIBILITY),
@@ -841,7 +851,6 @@ function* activitiesPageSaga() {
takeEvery(MAP_TOGGLE_GEOJSON_CACHE, handle_MAP_TOGGLE_GEOJSON_CACHE),
takeEvery(UserSettings.InitState.getSuccess, handle_USER_SETTINGS_GET_INITIAL_STATE_SUCCESS),
takeEvery(MAP_INIT_REQUEST, handle_MAP_INIT_REQUEST),
- takeEvery(MAP_INIT_FOR_RECORDSET, handle_MAP_INIT_FOR_RECORDSETS),
takeEvery(Activity.GeoJson.get, handle_ACTIVITIES_GEOJSON_GET_REQUEST),
takeEvery(ACTIVITIES_GEOJSON_REFETCH_ONLINE, handle_ACTIVITIES_GEOJSON_REFETCH_ONLINE),
takeEvery(IAPP_GEOJSON_GET_REQUEST, handle_IAPP_GEOJSON_GET_REQUEST),
diff --git a/app/src/state/sagas/map/dataAccess.ts b/app/src/state/sagas/map/dataAccess.ts
index c14969d6b..c1b19e6c3 100644
--- a/app/src/state/sagas/map/dataAccess.ts
+++ b/app/src/state/sagas/map/dataAccess.ts
@@ -11,11 +11,12 @@ import {
FILTERS_PREPPED_FOR_VECTOR_ENDPOINT,
IAPP_GEOJSON_GET_ONLINE,
IAPP_GEOJSON_GET_SUCCESS,
- IAPP_GET_IDS_FOR_RECORDSET_ONLINE
+ IAPP_GET_IDS_FOR_RECORDSET_ONLINE,
+ IAPP_GET_IDS_FOR_RECORDSET_SUCCESS
} from 'state/actions';
import { ACTIVITY_GEOJSON_SOURCE_KEYS, selectMap } from 'state/reducers/map';
import WhatsHere from 'state/actions/whatsHere/WhatsHere';
-import { RecordSetType, UserRecordSet } from 'interfaces/UserRecordSet';
+import { RecordSetType, UserRecordSet, UserRecordCacheStatus } from 'interfaces/UserRecordSet';
import { MOBILE } from 'state/build-time-config';
import { RecordCacheServiceFactory } from 'utils/record-cache/context';
import GeoShapes from 'constants/geoShapes';
@@ -77,16 +78,14 @@ export function* handle_PREP_FILTERS_FOR_VECTOR_ENDPOINT(action) {
return;
}
- yield put({
- type: FILTERS_PREPPED_FOR_VECTOR_ENDPOINT,
- payload: {
- filterObject: filterObject,
- recordSetID: action.payload.recordSetID,
- tableFiltersHash: action.payload.tableFiltersHash,
- recordSetType: recordset.recordSetType,
- cacheMetadata: recordset.cacheMetadata ?? null
- }
- });
+ const payload = {
+ filterObject: filterObject,
+ recordSetID: action.payload.recordSetID,
+ tableFiltersHash: action.payload.tableFiltersHash,
+ recordSetType: recordset.recordSetType
+ };
+
+ yield put({ type: FILTERS_PREPPED_FOR_VECTOR_ENDPOINT, payload });
} catch (e) {
console.error(e);
throw e;
@@ -115,12 +114,15 @@ export function* handle_ACTIVITIES_GET_IDS_FOR_RECORDSET_REQUEST(action) {
});
} else {
const recordSet = currentState.recordSets[action.payload.recordSetID] ?? null;
- if (recordSet?.cacheMetadata?.idList) {
+ if (recordSet.cacheMetadataStatus === UserRecordCacheStatus.CACHED) {
+ const service = yield RecordCacheServiceFactory.getPlatformInstance();
+ const ids = yield service.getIdList(action.payload.recordSetID);
+
yield put({
type: ACTIVITIES_GET_IDS_FOR_RECORDSET_SUCCESS,
payload: {
recordSetID: action.payload.recordSetID,
- IDList: recordSet.cacheMetadata.idList ?? [],
+ IDList: ids ?? [],
tableFiltersHash: action.payload.tableFiltersHash
}
});
@@ -155,6 +157,19 @@ export function* handle_IAPP_GET_IDS_FOR_RECORDSET_REQUEST(action) {
tableFiltersHash: action.payload.tableFiltersHash
}
});
+ } else {
+ const service = yield RecordCacheServiceFactory.getPlatformInstance();
+ if (yield service.isCached(action.payload.recordSetID)) {
+ const ids = yield service.getIdList(action.payload.recordSetID);
+ yield put({
+ type: IAPP_GET_IDS_FOR_RECORDSET_SUCCESS,
+ payload: {
+ recordSetID: action.payload.recordSetID,
+ IDList: ids,
+ tableFiltersHash: action.payload.tableFiltersHash
+ }
+ });
+ }
}
} catch (e) {
console.error(e);
@@ -221,11 +236,9 @@ export function* handle_ACTIVITIES_TABLE_ROWS_GET_REQUEST(action) {
}
if (userMobileOffline) {
- const recordSetIdList = yield select(
- (state) => state.UserSettings.recordSets[recordSetID].cacheMetadata.idList ?? []
- );
const service = yield RecordCacheServiceFactory.getPlatformInstance();
- const records = yield service.fetchPaginatedCachedRecords(recordSetIdList, page, limit);
+ const recordSetIdList = yield service.getIdList(recordSetID);
+ const records = yield service.getPaginatedCachedActivityRecords(recordSetIdList, page, limit);
yield put(
Activity.getRowsSuccess({
recordSetID: recordSetID,
@@ -275,9 +288,9 @@ export function* handle_IAPP_TABLE_ROWS_GET_REQUEST(action: PayloadAction
+ */
+abstract class BaseCacheService<
+ RepoMetadata,
+ RepositoryDownloadRequestSpec,
+ ProgressCallbackParams,
+ RepositoryStatusSchema
+> {
+ /** Update any details for a Repository */
+ protected abstract addOrUpdateRepository(repositoryId: RepoMetadata): Promise;
+
+ /** Remove one Repository from the collection along with its contentes */
+ public abstract deleteRepository(repositoryId: string): Promise;
+
+ /** Pull metadata for one Repository in the Collection */
+ public abstract getRepository(repositoryId: string): Promise;
+
+ /** List metadata for all Repositories */
+ public abstract listRepositories(): Promise;
+
+ /** Change the status for a given Repository */
+ public abstract setRepositoryStatus(repositoryId: string, status: RepositoryStatusSchema): Promise;
+
+ /** Download a new Repository along with its contents */
+ public abstract download(
+ spec: RepositoryDownloadRequestSpec,
+ progressCallback?: (currentProgress: ProgressCallbackParams) => void
+ ): Promise;
+
+ protected constructor() {}
+}
+
+export default BaseCacheService;
diff --git a/app/src/utils/filterRecordsetsByNetworkState.ts b/app/src/utils/filterRecordsetsByNetworkState.ts
index 77d62f15e..f9df48df9 100644
--- a/app/src/utils/filterRecordsetsByNetworkState.ts
+++ b/app/src/utils/filterRecordsetsByNetworkState.ts
@@ -8,7 +8,7 @@ import { UserRecordCacheStatus, UserRecordSet } from 'interfaces/UserRecordSet';
*/
const filterRecordsetsByNetworkState = (recordSets: Record, userOffline: boolean): string[] =>
Object.keys(recordSets).filter((set) => {
- return !userOffline || recordSets[set].cacheMetadata.status === UserRecordCacheStatus.CACHED;
+ return !userOffline || recordSets[set].cacheMetadataStatus === UserRecordCacheStatus.CACHED;
});
export default filterRecordsetsByNetworkState;
diff --git a/app/src/utils/getBoundingBoxFromRecordsetFilters.ts b/app/src/utils/getBoundingBoxFromRecordsetFilters.ts
index 2083d9b51..03d9c58b2 100644
--- a/app/src/utils/getBoundingBoxFromRecordsetFilters.ts
+++ b/app/src/utils/getBoundingBoxFromRecordsetFilters.ts
@@ -1,5 +1,5 @@
import bbox from '@turf/bbox';
-import { UserRecordSet } from 'interfaces/UserRecordSet';
+import { RecordSetType, UserRecordSet } from 'interfaces/UserRecordSet';
import { getCurrentJWT } from 'state/sagas/auth/auth';
import { getSelectColumnsByRecordSetType } from 'state/sagas/map/dataAccess';
import { parse } from 'wkt';
@@ -9,13 +9,16 @@ const getBoundingBoxFromRecordsetFilters = async (recordSet: UserRecordSet): Pro
const { recordSetType } = recordSet;
const filterObj = {
recordSetType: recordSetType,
- sortColumn: 'short_id',
+ sortColumn: recordSetType === RecordSetType.Activity ? 'short_id' : 'site_id',
sortOrder: 'DESC',
tableFilters: recordSet.tableFilters,
selectColumns: getSelectColumnsByRecordSetType(recordSetType)
};
-
- const data = await fetch(`${CONFIGURATION_API_BASE}/api/v2/activities/bbox`, {
+ const url =
+ recordSetType === RecordSetType.Activity
+ ? `${CONFIGURATION_API_BASE}/api/v2/activities/bbox`
+ : `${CONFIGURATION_API_BASE}/api/v2/IAPP/bbox`;
+ const data = await fetch(url, {
method: 'POST',
headers: {
Authorization: await getCurrentJWT(),
@@ -26,10 +29,10 @@ const getBoundingBoxFromRecordsetFilters = async (recordSet: UserRecordSet): Pro
const [minLongitude, minLatitude, maxLongitude, maxLatitude] = bbox(parse(data.bbox));
return {
- minLatitude: minLatitude,
- maxLatitude: maxLongitude,
- minLongitude: minLongitude,
- maxLongitude: maxLatitude
+ minLatitude,
+ maxLatitude,
+ minLongitude,
+ maxLongitude
};
};
diff --git a/app/src/utils/record-cache/context.ts b/app/src/utils/record-cache/context.ts
index c774bd903..ebeee2cea 100644
--- a/app/src/utils/record-cache/context.ts
+++ b/app/src/utils/record-cache/context.ts
@@ -1,9 +1,10 @@
import { Platform, PLATFORM } from 'state/build-time-config';
+import { RecordCacheService } from 'utils/record-cache/index';
import { SQLiteRecordCacheService } from 'utils/record-cache/sqlite-cache';
import { LocalForageRecordCacheService } from 'utils/record-cache/localforage-cache';
class RecordCacheServiceFactory {
- static async getPlatformInstance() {
+ static async getPlatformInstance(): Promise {
if (PLATFORM == Platform.IOS) {
return SQLiteRecordCacheService.getInstance();
}
diff --git a/app/src/utils/record-cache/index.ts b/app/src/utils/record-cache/index.ts
index 7978a72c2..9b0c91332 100644
--- a/app/src/utils/record-cache/index.ts
+++ b/app/src/utils/record-cache/index.ts
@@ -5,6 +5,7 @@ import { RecordSetType, UserRecordCacheStatus } from 'interfaces/UserRecordSet';
import { GeoJSONSourceSpecification } from 'maplibre-gl';
import { getCurrentJWT } from 'state/sagas/auth/auth';
import { getSelectColumnsByRecordSetType } from 'state/sagas/map/dataAccess';
+import BaseCacheService from 'utils/base-classes/BaseCacheService';
import { RepositoryBoundingBoxSpec } from 'utils/tile-cache';
export enum IappRecordMode {
@@ -25,7 +26,7 @@ export interface RecordCacheDownloadRequestSpec {
* @property { GeoJSONSourceSpecification } cachedCentroid Cached Points for high map layers
* @property { UserRecordCacheStatus } status Cache Status.
*/
-export interface RecordCacheAddSpec {
+export interface RepositoryMetadata {
setId: string;
cacheTime: Date;
cachedIds: string[];
@@ -58,47 +59,57 @@ export interface CacheDownloadSpec {
recordSetType: RecordSetType;
}
-abstract class RecordCacheService {
+abstract class RecordCacheService extends BaseCacheService<
+ RepositoryMetadata,
+ CacheDownloadSpec,
+ RecordCacheProgressCallbackParameters,
+ UserRecordCacheStatus
+> {
private readonly RECORDS_BETWEEN_PROGRESS_UPDATES = 25;
- protected constructor() {}
+
+ protected constructor() {
+ super();
+ }
static async getInstance(): Promise {
throw new Error('unimplemented in abstract base class');
}
+ protected abstract addOrUpdateRepository(spec: RepositoryMetadata): Promise;
- abstract saveActivity(id: string, data: unknown): Promise;
+ protected abstract deleteCachedRecordsFromIds(idsToDelete: string[], recordSetType: RecordSetType): Promise;
- abstract saveIapp(id: string, iappRecord: unknown, iappTableRow: unknown): Promise;
+ /** */
+ public abstract loadActivity(id: string): Promise;
- abstract deleteCachedRecordsFromIds(idsToDelete: string[], recordSetType: RecordSetType): Promise;
+ public abstract loadIapp(id: string, type: IappRecordMode): Promise;
- abstract loadActivity(id: string): Promise;
+ protected abstract saveActivity(id: string, data: unknown): Promise;
- abstract loadIapp(id: string, type: IappRecordMode): Promise;
+ protected abstract saveIapp(id: string, iappRecord: unknown, iappTableRow: unknown): Promise;
- abstract fetchPaginatedCachedIappRecords(
+ public abstract getPaginatedCachedActivityRecords(
recordSetIdList: string[],
page: number,
limit: number
- ): Promise;
-
- abstract fetchPaginatedCachedRecords(recordSetIdList: string[], page: number, limit: number): Promise;
-
- abstract addOrUpdateRepository(spec: RecordCacheAddSpec): Promise;
+ ): Promise;
- abstract deleteRepository(repositoryId: string): Promise;
+ public abstract getPaginatedCachedIappRecords(
+ recordSetIdList: string[],
+ page: number,
+ limit: number
+ ): Promise;
- abstract listRepositories(): Promise;
+ public abstract isCached(repositoryId: string): Promise;
- abstract loadIappRecordsetSourceMetadata(ids: string[]): Promise;
+ public abstract getIdList(repositoryId: string): Promise;
- abstract loadRecordsetSourceMetadata(ids: string[]): Promise;
+ protected abstract createIappRecordsetSourceMetadata(ids: string[]): Promise;
- abstract setRepositoryStatus(repositoryId: string, status: UserRecordCacheStatus): Promise;
+ protected abstract createActivityRecordsetSourceMetadata(ids: string[]): Promise;
abstract checkForAbort(id: string): Promise;
- async downloadCache(spec: CacheDownloadSpec): Promise> {
+ public async download(spec: CacheDownloadSpec): Promise {
const args = {
idsToCache: spec.idsToCache,
setId: spec.setId,
@@ -119,33 +130,35 @@ abstract class RecordCacheService {
bbox: spec.bbox
});
+ let downloadCompleted = true;
if (spec.recordSetType === RecordSetType.Activity && (await this.downloadActivity(args))) {
- Object.assign(responseData, await this.loadRecordsetSourceMetadata(spec.idsToCache));
+ Object.assign(responseData, await this.createActivityRecordsetSourceMetadata(spec.idsToCache));
} else if (spec.recordSetType === RecordSetType.IAPP && (await this.downloadIapp(args))) {
- Object.assign(responseData, await this.loadIappRecordsetSourceMetadata(spec.idsToCache));
+ Object.assign(responseData, await this.createIappRecordsetSourceMetadata(spec.idsToCache));
} else {
+ downloadCompleted = false;
this.deleteRepository(spec.setId);
- throw Error('Early Exit');
}
- await this.addOrUpdateRepository({
- setId: spec.setId,
- cacheTime: new Date(),
- cachedIds: spec.idsToCache,
- recordSetType: spec.recordSetType,
- status: UserRecordCacheStatus.CACHED,
- cachedGeoJson: responseData.cachedGeoJson,
- cachedCentroid: responseData.cachedCentroid,
- bbox: spec.bbox
- });
-
- return responseData;
+ if (downloadCompleted) {
+ await this.addOrUpdateRepository({
+ setId: spec.setId,
+ cacheTime: new Date(),
+ cachedIds: spec.idsToCache,
+ recordSetType: spec.recordSetType,
+ status: UserRecordCacheStatus.CACHED,
+ cachedGeoJson: responseData.cachedGeoJson,
+ cachedCentroid: responseData.cachedCentroid,
+ bbox: spec.bbox
+ });
+ }
+ return downloadCompleted;
}
/**
* Download Records for IAPP Given a list of IDs
* @returns { boolean } download was successful
*/
- async downloadIapp(
+ private async downloadIapp(
spec: RecordCacheDownloadRequestSpec,
progressCallback?: (currentProgress: RecordCacheProgressCallbackParameters) => void
): Promise {
@@ -195,7 +208,7 @@ abstract class RecordCacheService {
* Download Records for Activities Given a list of IDs
* @returns { boolean } download was successful
*/
- async downloadActivity(
+ private async downloadActivity(
spec: RecordCacheDownloadRequestSpec,
progressCallback?: (currentProgress: RecordCacheProgressCallbackParameters) => void
): Promise {
@@ -216,7 +229,7 @@ abstract class RecordCacheService {
}
return !abort;
}
- async stopDownload(repositoryId: string): Promise {
+ public async stopDownload(repositoryId: string): Promise {
const repositories = await this.listRepositories();
const foundIndex = repositories.findIndex((repo) => repo.setId === repositoryId);
if (foundIndex === -1) throw Error(`Repository ${repositoryId} wasn't found`);
diff --git a/app/src/utils/record-cache/localforage-cache.ts b/app/src/utils/record-cache/localforage-cache.ts
index bdb4c3172..6ba5efb5f 100644
--- a/app/src/utils/record-cache/localforage-cache.ts
+++ b/app/src/utils/record-cache/localforage-cache.ts
@@ -3,7 +3,7 @@ import localForage from 'localforage';
import centroid from '@turf/centroid';
import {
IappRecordMode,
- RecordCacheAddSpec,
+ RepositoryMetadata,
RecordCacheService,
RecordSetSourceMetadata
} from 'utils/record-cache/index';
@@ -32,6 +32,26 @@ class LocalForageRecordCacheService extends RecordCacheService {
return LocalForageRecordCacheService._instance;
}
+ async isCached(repositoryId: string): Promise {
+ try {
+ return (await this.getRepository(repositoryId)).status === UserRecordCacheStatus.CACHED;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ async getRepository(repositoryId: string): Promise {
+ const repos = await this.listRepositories();
+ const foundIndex = repos.findIndex((p) => p.setId === repositoryId);
+ if (foundIndex === -1) throw Error(`Repository ${repositoryId} not found`);
+
+ return repos[foundIndex];
+ }
+
+ async getIdList(repositoryId: string): Promise {
+ return (await this.getRepository(repositoryId)).cachedIds ?? [];
+ }
+
async saveActivity(id: string, data: unknown): Promise {
if (this.store == null) {
throw new Error('cache not available');
@@ -60,6 +80,7 @@ class LocalForageRecordCacheService extends RecordCacheService {
}
return true;
}
+
async saveIapp(id: string, iappRecord: IappRecord, iappTableRow: IappTableRow): Promise {
if (this.store == null) {
throw new Error('cache not available');
@@ -79,7 +100,7 @@ class LocalForageRecordCacheService extends RecordCacheService {
return data[type];
}
- async fetchPaginatedCachedIappRecords(
+ async getPaginatedCachedIappRecords(
recordSetIdList: string[],
page: number,
limit: number,
@@ -119,7 +140,11 @@ class LocalForageRecordCacheService extends RecordCacheService {
* @param limit Maximum results per page
* @returns { UserRecord[] } Filter Objects
*/
- async fetchPaginatedCachedRecords(recordSetIdList: string[], page: number, limit: number): Promise {
+ async getPaginatedCachedActivityRecords(
+ recordSetIdList: string[],
+ page: number,
+ limit: number
+ ): Promise {
if (recordSetIdList?.length === 0) {
return [];
}
@@ -139,7 +164,7 @@ class LocalForageRecordCacheService extends RecordCacheService {
* @param ids ids to filter
* @returns { RecordSetSourceMetadata } Returns cached GeoJson, all IAPP Sites are Points.
*/
- async loadIappRecordsetSourceMetadata(ids: string[]): Promise {
+ async createIappRecordsetSourceMetadata(ids: string[]): Promise {
const geoJsonArr: any[] = [];
for (const id of ids) {
const data: IappRecord = await this.loadIapp(id, IappRecordMode.Row);
@@ -163,7 +188,7 @@ class LocalForageRecordCacheService extends RecordCacheService {
* @param ids ids to filter
* @returns { RecordSetSourceMetadata } Two formatted queries for High/Low zoom layers
*/
- async loadRecordsetSourceMetadata(ids: string[]): Promise {
+ async createActivityRecordsetSourceMetadata(ids: string[]): Promise {
const centroidArr: any[] = [];
const geoJsonArr: any[] = [];
@@ -237,7 +262,7 @@ class LocalForageRecordCacheService extends RecordCacheService {
* @desc Create or Update an entry in the cachedSet Repository
* @param newSet Data to update
*/
- async addOrUpdateRepository(newSet: RecordCacheAddSpec): Promise {
+ async addOrUpdateRepository(newSet: RepositoryMetadata): Promise {
if (this.store == null) {
throw new Error('cache not available');
}
@@ -253,12 +278,12 @@ class LocalForageRecordCacheService extends RecordCacheService {
await this.store.setItem(LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY, cachedSets);
}
- async listRepositories(): Promise {
+ async listRepositories(): Promise {
if (this.store == null) {
return [];
}
- const metadata: RecordCacheAddSpec[] =
+ const metadata: RepositoryMetadata[] =
(await this.store.getItem(LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY)) ?? [];
if (metadata == null) {
console.error('expected key not found');
diff --git a/app/src/utils/record-cache/sqlite-cache.ts b/app/src/utils/record-cache/sqlite-cache.ts
index f4d8fc35b..69b29c742 100644
--- a/app/src/utils/record-cache/sqlite-cache.ts
+++ b/app/src/utils/record-cache/sqlite-cache.ts
@@ -8,7 +8,7 @@ import { RecordSetType, UserRecordCacheStatus } from 'interfaces/UserRecordSet';
import { GeoJSONSourceSpecification } from 'maplibre-gl';
import {
IappRecordMode,
- RecordCacheAddSpec,
+ RepositoryMetadata,
RecordCacheService,
RecordSetSourceMetadata
} from 'utils/record-cache/index';
@@ -64,7 +64,6 @@ class SQLiteRecordCacheService extends RecordCacheService {
private static _instance: SQLiteRecordCacheService;
private cacheDB: SQLiteDBConnection | null = null;
-
protected constructor() {
super();
}
@@ -77,7 +76,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
return SQLiteRecordCacheService._instance;
}
- async addOrUpdateRepository(spec: RecordCacheAddSpec): Promise {
+ async addOrUpdateRepository(spec: RepositoryMetadata): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
}
@@ -98,6 +97,44 @@ class SQLiteRecordCacheService extends RecordCacheService {
}
}
+ async getRepository(repositoryId: string): Promise {
+ if (this.cacheDB == null) {
+ throw new Error(CACHE_UNAVAILABLE);
+ }
+ const repoData = await this.cacheDB.query(
+ //language=SQLite
+ `SELECT DATA
+ FROM CACHE_METADATA
+ WHERE SET_ID = ?
+ LIMIT 1`,
+ [repositoryId]
+ );
+ return JSON.parse(repoData?.values?.[0]['DATA']) ?? {};
+ }
+
+ async isCached(repositoryId: string): Promise {
+ if (this.cacheDB == null) {
+ throw new Error(CACHE_UNAVAILABLE);
+ }
+ const metadata = await this.cacheDB.query(
+ //language=SQLite
+ `SELECT STATUS
+ FROM CACHE_METADATA
+ WHERE SET_ID = ?
+ LIMIT 1
+ `,
+ [repositoryId]
+ );
+ return metadata?.values?.[0]?.['STATUS'] === UserRecordCacheStatus.CACHED;
+ }
+
+ async getIdList(repositoryId: string): Promise {
+ if (this.cacheDB == null) {
+ throw Error(CACHE_UNAVAILABLE);
+ }
+ return (await this.getRepository(repositoryId)).cachedIds ?? [];
+ }
+
async deleteRepository(repositoryId: string): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
@@ -107,11 +144,11 @@ class SQLiteRecordCacheService extends RecordCacheService {
`SELECT DATA
FROM CACHE_METADATA`
);
- const repositoryMetadata: RecordCacheAddSpec[] =
+ const repositoryMetadata: RepositoryMetadata[] =
rawRepositoryMetadata?.values?.map((set) => JSON.parse(set['DATA'])) ?? [];
const targetIndex = repositoryMetadata.findIndex((set) => set.setId === repositoryId);
- if (targetIndex === -1) throw Error('Repository not found');
+ if (targetIndex === -1) return;
const { cachedIds, recordSetType } = repositoryMetadata[targetIndex];
@@ -133,50 +170,32 @@ class SQLiteRecordCacheService extends RecordCacheService {
);
}
- async listRepositories(): Promise {
+ async listRepositories(): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
}
const repositories = await this.cacheDB.query(
- //language=SQLite
- `SELECT *
- FROM CACHE_METADATA`
- );
- return repositories?.values?.map((entry) => JSON.parse(entry['DATA']) as RecordCacheAddSpec) ?? [];
- }
-
- /**
- * @desc Helper method to fetch and parse repo metadata
- */
- private async getRepoData(repositoryId: string) {
- if (this.cacheDB == null) {
- throw new Error(CACHE_UNAVAILABLE);
- }
- const repoData = await this.cacheDB.query(
//language=SQLite
`SELECT DATA
- FROM CACHE_METADATA
- WHERE SET_ID = ?`,
- [repositoryId]
+ FROM CACHE_METADATA`
);
- return JSON.parse(repoData?.values?.[0]['DATA']) ?? {};
+ const response = repositories?.values?.map((entry) => JSON.parse(entry['DATA']) as RepositoryMetadata) ?? [];
+ return response;
}
async setRepositoryStatus(repositoryId: string, status: UserRecordCacheStatus): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
}
- const currData = await this.getRepoData(repositoryId);
+ const currData = await this.getRepository(repositoryId);
+ if (Object.keys(currData).length === 0) return; // Repo doesn't exist.
currData.status = status;
- await this.cacheDB.query(
- //language=SQLite
- `UPDATE CACHE_METADATA
- SET STATUS = ?,
- CACHE_TIME = ?,
- DATA = ?
- WHERE SET_ID = ?`,
- [status, JSON.stringify(currData.cacheTime), JSON.stringify(currData), repositoryId]
- );
+
+ this.addOrUpdateRepository({
+ ...currData,
+ setId: repositoryId,
+ status: status
+ });
}
async checkForAbort(repositoryId: string): Promise {
@@ -207,7 +226,11 @@ class SQLiteRecordCacheService extends RecordCacheService {
* @param limit Maximum results per page
* @returns { UserRecord[] } Filter Objects
*/
- async fetchPaginatedCachedRecords(recordSetIdList: string[], page: number, limit: number): Promise {
+ async getPaginatedCachedActivityRecords(
+ recordSetIdList: string[],
+ page: number,
+ limit: number
+ ): Promise {
if (!recordSetIdList || recordSetIdList.length === 0) {
return [];
}
@@ -240,7 +263,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
return response;
}
- async fetchPaginatedCachedIappRecords(recordSetIdList: string[], page: number, limit: number): Promise {
+ async getPaginatedCachedIappRecords(recordSetIdList: string[], page: number, limit: number): Promise {
if (!recordSetIdList || recordSetIdList.length === 0) {
return [];
}
@@ -302,6 +325,12 @@ class SQLiteRecordCacheService extends RecordCacheService {
const stringified = JSON.stringify(data);
const short_id = (data as Record)?.short_id;
const geometry = (data as Record)?.geometry;
+ geometry.forEach((_, i) => {
+ geometry[i].properties = {
+ name: short_id,
+ description: id
+ };
+ });
const geojson = JSON.stringify(geometry) ?? null;
await this.cacheDB.query(
//language=SQLite
@@ -356,7 +385,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
return JSON.parse(result.values[0][dataType]);
}
- async loadIappRecordsetSourceMetadata(ids: string[]): Promise {
+ async createIappRecordsetSourceMetadata(ids: string[]): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
}
@@ -378,7 +407,8 @@ class SQLiteRecordCacheService extends RecordCacheService {
};
return { cachedGeoJson };
}
- async loadRecordsetSourceMetadata(ids: string[]): Promise {
+
+ async createActivityRecordsetSourceMetadata(ids: string[]): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
}
@@ -395,9 +425,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
results?.values?.forEach((item) => {
try {
- const label = item['SHORT_ID'];
JSON.parse(item['GEOJSON'])?.forEach((feature: Feature) => {
- feature.properties = { name: label };
centroidArr.push(centroid(feature));
geoJsonArr.push(feature);
});
@@ -405,6 +433,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
console.error('Error parsing record:', e);
}
});
+
const cachedCentroid: GeoJSONSourceSpecification = {
type: 'geojson',
data: {
@@ -421,6 +450,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
};
return { cachedCentroid, cachedGeoJson };
}
+
async deleteCachedRecordsFromIds(idsToDelete: string[], recordSetType: RecordSetType): Promise {
if (this.cacheDB == null) {
throw new Error(CACHE_UNAVAILABLE);
@@ -433,7 +463,7 @@ class SQLiteRecordCacheService extends RecordCacheService {
const RECORD_TABLE = RecordsToTable[recordSetType];
const BATCH_AMOUNT = 100;
- this.cacheDB.beginTransaction();
+ await this.cacheDB.beginTransaction();
try {
for (let i = 0; i < idsToDelete.length; i += BATCH_AMOUNT) {
const sliced = idsToDelete.slice(i, Math.min(i + BATCH_AMOUNT, idsToDelete.length));
@@ -444,12 +474,13 @@ class SQLiteRecordCacheService extends RecordCacheService {
[...sliced]
);
}
- this.cacheDB.commitTransaction();
+ await this.cacheDB.commitTransaction();
} catch (e) {
- this.cacheDB.rollbackTransaction();
+ await this.cacheDB.rollbackTransaction();
throw e;
}
}
+
private async initializeRecordCache(sqlite: SQLiteConnection) {
// Hold Migrations as named variable so we can use length to update the Db version automagically
// Note: toVersion must be an integer.
diff --git a/app/src/utils/tile-cache/context.ts b/app/src/utils/tile-cache/context.ts
index 3428863e5..b5568412f 100644
--- a/app/src/utils/tile-cache/context.ts
+++ b/app/src/utils/tile-cache/context.ts
@@ -5,7 +5,7 @@ import { SQLiteTileCacheService } from 'utils/tile-cache/sqlite-cache';
import { LocalForageCacheService } from 'utils/tile-cache/localforage-cache';
class TileCacheServiceFactory {
- static async getPlatformInstance() {
+ static async getPlatformInstance(): Promise {
if ([Platform.IOS, Platform.ANDROID].includes(PLATFORM)) {
return SQLiteTileCacheService.getInstance();
}
diff --git a/app/src/utils/tile-cache/index.ts b/app/src/utils/tile-cache/index.ts
index 543db6a7f..a2e9a1373 100644
--- a/app/src/utils/tile-cache/index.ts
+++ b/app/src/utils/tile-cache/index.ts
@@ -1,3 +1,4 @@
+import BaseCacheService from 'utils/base-classes/BaseCacheService';
import { base64toBuffer, lat2tile, long2tile } from 'utils/tile-cache/helpers';
// base64-encoded blank tile image 256x256 (opaque, light blue)
@@ -42,7 +43,6 @@ enum RepositoryStatus {
FAILED = 'FAILED',
UNKNOWN = 'UNKNOWN'
}
-
interface TilePromise {
id: string;
url: string;
@@ -50,6 +50,7 @@ interface TilePromise {
y: number;
z: number;
}
+
export interface TileCacheProgressCallbackParameters {
repository: string;
message: string;
@@ -64,8 +65,15 @@ export interface RepositoryStatistics {
tileCount: number;
}
-abstract class TileCacheService {
- protected constructor() {}
+abstract class TileCacheService extends BaseCacheService<
+ RepositoryMetadata,
+ RepositoryDownloadRequestSpec,
+ TileCacheProgressCallbackParameters,
+ RepositoryStatus
+> {
+ protected constructor() {
+ super();
+ }
static generateFallbackTile(): TileData {
return {
@@ -106,13 +114,7 @@ abstract class TileCacheService {
abstract setTile(repository: string, z: number, x: number, y: number, tileData: Uint8Array): Promise;
- abstract getRepository(id: string): Promise;
-
- abstract listRepositories(): Promise;
-
- abstract deleteRepository(repository: string): Promise;
-
- abstract setRepositoryStatus(repository: string, status: RepositoryStatus): Promise;
+ protected abstract addOrUpdateRepository(spec: RepositoryMetadata): Promise;
private async downloadTile(tileDetails: TilePromise): Promise {
const { id, url, x, y, z } = tileDetails;
@@ -151,7 +153,7 @@ abstract class TileCacheService {
const executing = new Set();
try {
- await this.addRepository({
+ await this.addOrUpdateRepository({
id: spec.id,
status: RepositoryStatus.DOWNLOADING,
maxZoom: spec.maxZoom,
@@ -234,8 +236,6 @@ abstract class TileCacheService {
public abstract updateDescription(repository: string, newDescription: string): Promise;
protected abstract cleanupOrphanTiles(): Promise;
-
- protected abstract addRepository(spec: RepositoryMetadata): Promise;
}
export { TileCacheService, FALLBACK_IMAGE, RepositoryStatus };
diff --git a/app/src/utils/tile-cache/localforage-cache.ts b/app/src/utils/tile-cache/localforage-cache.ts
index d82b9a806..ffdac1c13 100644
--- a/app/src/utils/tile-cache/localforage-cache.ts
+++ b/app/src/utils/tile-cache/localforage-cache.ts
@@ -21,7 +21,7 @@ interface TileKey {
class LocalForageCacheService extends TileCacheService {
private static _instance: LocalForageCacheService;
- private static REPOSITORY_METADATA_KEY = 'repositories';
+ private static readonly REPOSITORY_METADATA_KEY = 'repositories';
private store: LocalForage | null = null;
@@ -222,7 +222,7 @@ class LocalForageCacheService extends TileCacheService {
}
}
- protected async addRepository(spec: RepositoryMetadata) {
+ protected async addOrUpdateRepository(spec: RepositoryMetadata) {
if (this.store == null) {
throw new Error('cache not available');
}
@@ -230,11 +230,11 @@ class LocalForageCacheService extends TileCacheService {
const repositories = await this.listRepositories();
const foundIndex = repositories.findIndex((p) => p.id == spec.id);
if (foundIndex !== -1) {
- throw new Error('repository already exists');
+ repositories[foundIndex] = spec;
+ } else {
+ repositories.push(spec);
}
- repositories.push(spec);
-
await this.store.setItem(LocalForageCacheService.REPOSITORY_METADATA_KEY, repositories);
}
diff --git a/app/src/utils/tile-cache/sqlite-cache.ts b/app/src/utils/tile-cache/sqlite-cache.ts
index eaf0e9569..2b9dc56bf 100644
--- a/app/src/utils/tile-cache/sqlite-cache.ts
+++ b/app/src/utils/tile-cache/sqlite-cache.ts
@@ -190,7 +190,7 @@ class SQLiteTileCacheService extends TileCacheService {
);
}
- protected async addRepository(spec: RepositoryMetadata): Promise {
+ protected async addOrUpdateRepository(spec: RepositoryMetadata): Promise {
if (this.cacheDB == null) {
throw new Error('cache not available');
}
@@ -207,7 +207,15 @@ class SQLiteTileCacheService extends TileCacheService {
MIN_LONGITUDE,
MAX_LONGITUDE)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- `,
+ ON CONFLICT(TILESET)
+ DO UPDATE SET
+ DESCRIPTION = excluded.DESCRIPTION,
+ STATUS = excluded.STATUS,
+ MAX_ZOOM = excluded.MAX_ZOOM,
+ MIN_LATITUDE = excluded.MIN_LATITUDE,
+ MAX_LATITUDE = excluded.MAX_LATITUDE,
+ MIN_LONGITUDE = excluded.MIN_LONGITUDE,
+ MAX_LONGITUDE = excluded.MAX_LONGITUDE`,
[
spec.id,
spec.description,