diff --git a/scripts/build-backend.mjs b/scripts/build-backend.mjs index cd7a3bd56..05241eb31 100755 --- a/scripts/build-backend.mjs +++ b/scripts/build-backend.mjs @@ -75,7 +75,9 @@ const KEEP_THESE = [ // Static folders referenced by @mapeo/core code 'node_modules/@mapeo/core/drizzle', // zip file that is the default config - 'node_modules/@mapeo/default-config/dist/mapeo-default-config.mapeoconfig' + 'node_modules/@mapeo/default-config/dist/mapeo-default-config.mapeoconfig', + // Offline fallback map + 'node_modules/mapeo-offline-map', ]; for (const name of KEEP_THESE) { diff --git a/src/backend/index.js b/src/backend/index.js index 2c50554fb..8997a3516 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -9,6 +9,10 @@ const MIGRATIONS_FOLDER_PATH = new URL( './node_modules/@mapeo/core/drizzle', import.meta.url, ).pathname +const FALLBACK_MAP_PATH = new URL( + './node_modules/mapeo-offline-map', + import.meta.url, +).pathname const DEFAULT_CONFIG_PATH = new URL( './node_modules/@mapeo/default-config/dist/mapeo-default-config.mapeoconfig', @@ -41,6 +45,7 @@ try { migrationsFolderPath: MIGRATIONS_FOLDER_PATH, sharedStoragePath: values.sharedStoragePath, defaultConfigPath: DEFAULT_CONFIG_PATH, + fallbackMapPath: FALLBACK_MAP_PATH, }).catch((err) => { console.error('Server startup error:', err) }) diff --git a/src/backend/package-lock.json b/src/backend/package-lock.json index 9c872f01a..7b6d170be 100644 --- a/src/backend/package-lock.json +++ b/src/backend/package-lock.json @@ -13,7 +13,8 @@ "@mapeo/core": "9.0.0-alpha.9", "@mapeo/default-config": "^4.0.0-alpha.2", "@mapeo/ipc": "0.5.0", - "debug": "^4.3.4" + "debug": "^4.3.4", + "mapeo-offline-map": "^2.0.0" }, "devDependencies": { "@digidem/types": "~2.1.0", @@ -3732,6 +3733,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mapeo-offline-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mapeo-offline-map/-/mapeo-offline-map-2.0.0.tgz", + "integrity": "sha512-/a2BVgSwcL0EGqNxc7MWaApEJ0uLxr/scaZimd/2RvihDtvaNsLQurHIAjXJnY5lj4to5fBFBpcWXntdZoRBdg==" + }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -8467,6 +8473,11 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.2.tgz", "integrity": "sha512-K6K2NgKnTXimT3779/4KxSvobxOtMmx1LBZ3NwRxT/MDIR3Br/fQ4Q+WCX5QxjyUR8zg5+RV9Tbf2c5pAWTD2A==" }, + "mapeo-offline-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mapeo-offline-map/-/mapeo-offline-map-2.0.0.tgz", + "integrity": "sha512-/a2BVgSwcL0EGqNxc7MWaApEJ0uLxr/scaZimd/2RvihDtvaNsLQurHIAjXJnY5lj4to5fBFBpcWXntdZoRBdg==" + }, "marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", diff --git a/src/backend/package.json b/src/backend/package.json index f3b0ed55b..0b9b393d1 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -16,7 +16,8 @@ "@mapeo/core": "9.0.0-alpha.9", "@mapeo/default-config": "^4.0.0-alpha.2", "@mapeo/ipc": "0.5.0", - "debug": "^4.3.4" + "debug": "^4.3.4", + "mapeo-offline-map": "^2.0.0" }, "devDependencies": { "@digidem/types": "~2.1.0", diff --git a/src/backend/src/app.js b/src/backend/src/app.js index 253dfc85d..a6bf90a68 100644 --- a/src/backend/src/app.js +++ b/src/backend/src/app.js @@ -5,7 +5,13 @@ import { createRequire } from 'module' const require = createRequire(import.meta.url) /** @type {import('../types/rn-bridge.js')} */ const rnBridge = require('rn-bridge') -import { MapeoManager, FastifyController } from '@mapeo/core' +import { + MapeoManager, + FastifyController, + MapeoMapsFastifyPlugin, + MapeoStaticMapsFastifyPlugin, + MapeoOfflineFallbackMapFastifyPlugin, +} from '@mapeo/core' import { createMapeoServer } from '@mapeo/ipc' import Fastify from 'fastify' @@ -16,6 +22,10 @@ import { ServerStatus } from './status.js' const DB_DIR_NAME = 'sqlite-dbs' const CORE_STORAGE_DIR_NAME = 'core-storage' +const MAPBOX_ACCESS_TOKEN = + 'pk.eyJ1IjoiZGlnaWRlbSIsImEiOiJjbHRyaGh3cm0wN3l4Mmpsam95NDI3c2xiIn0.daq2iZFZXQ08BD0VZWAGUw' +const DEFAULT_ONLINE_MAP_STYLE_URL = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v11?access_token=${MAPBOX_ACCESS_TOKEN}` + const log = debug('mapeo:app') // Set these up as soon as possible (e.g. before the init function) @@ -48,6 +58,7 @@ process.on('exit', (code) => { * @param {string} options.migrationsFolderPath * @param {string} options.sharedStoragePath Path to app-specific external file storage folder * @param {string} options.defaultConfigPath + * @param {string} options.fallbackMapPath Path to app-specific external file storage folder * */ export async function init({ @@ -56,6 +67,7 @@ export async function init({ migrationsFolderPath, sharedStoragePath, defaultConfigPath, + fallbackMapPath, }) { log('Starting app...') log(`Device version is ${version}`) @@ -63,13 +75,30 @@ export async function init({ const privateStorageDir = rnBridge.app.datadir() const dbDir = join(privateStorageDir, DB_DIR_NAME) const indexDir = join(privateStorageDir, CORE_STORAGE_DIR_NAME) + const staticStylesDir = join(sharedStoragePath, 'styles') mkdirSync(dbDir, { recursive: true }) mkdirSync(indexDir, { recursive: true }) + mkdirSync(staticStylesDir, { recursive: true }) const fastify = Fastify() const fastifyController = new FastifyController({ fastify }) + // Register maps plugins + fastify.register(MapeoStaticMapsFastifyPlugin, { + prefix: 'static', + staticRootDir: staticStylesDir, + }) + fastify.register(MapeoOfflineFallbackMapFastifyPlugin, { + prefix: 'fallback', + styleJsonPath: join(fallbackMapPath, 'style.json'), + sourcesDir: join(fallbackMapPath, 'dist'), + }) + fastify.register(MapeoMapsFastifyPlugin, { + prefix: 'maps', + defaultOnlineStyleUrl: DEFAULT_ONLINE_MAP_STYLE_URL, + }) + const manager = new MapeoManager({ rootKey, dbFolder: dbDir, diff --git a/src/frontend/hooks/server/mapStyleUrl.ts b/src/frontend/hooks/server/mapStyleUrl.ts new file mode 100644 index 000000000..077d355ec --- /dev/null +++ b/src/frontend/hooks/server/mapStyleUrl.ts @@ -0,0 +1,16 @@ +import {useQuery} from '@tanstack/react-query'; + +import {useApi} from '../../contexts/ApiContext'; + +export const MAP_STYLE_URL_KEY = 'map_style_url'; + +export function useMapStyleUrl() { + const api = useApi(); + + return useQuery({ + queryKey: [MAP_STYLE_URL_KEY], + queryFn: () => { + return api.getMapStyleJsonUrl(); + }, + }); +} diff --git a/src/frontend/screens/MapScreen/index.tsx b/src/frontend/screens/MapScreen/index.tsx index 2d80fbd3e..3ca342c04 100644 --- a/src/frontend/screens/MapScreen/index.tsx +++ b/src/frontend/screens/MapScreen/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import Mapbox from '@rnmapbox/maps'; + import config from '../../../config.json'; import {IconButton} from '../../sharedComponents/IconButton'; import { @@ -21,6 +22,7 @@ import {GPSPermissionsModal} from './GPSPermissions/GPSPermissionsModal'; import {TrackPathLayer} from './track/TrackPathLayer'; import {UserLocation} from './UserLocation'; import {useSharedLocationContext} from '../../contexts/SharedLocationContext'; +import {useMapStyleUrl} from '../../hooks/server/mapStyleUrl'; // This is the default zoom used when the map first loads, and also the zoom // that the map will zoom to if the user clicks the "Locate" button and the @@ -30,8 +32,6 @@ const DEFAULT_ZOOM = 12; Mapbox.setAccessToken(config.mapboxAccessToken); const MIN_DISPLACEMENT = 3; -export const MAP_STYLE = Mapbox.StyleURL.Outdoors; - export const MapScreen = () => { const [zoom, setZoom] = React.useState(DEFAULT_ZOOM); const [isFinishedLoading, setIsFinishedLoading] = React.useState(false); @@ -45,6 +45,8 @@ export const MapScreen = () => { const locationServicesEnabled = !!locationProviderStatus?.locationServicesEnabled; + const styleUrlQuery = useMapStyleUrl(); + const handleAddPress = () => { newDraft(); navigate('PresetChooser'); @@ -75,7 +77,7 @@ export const MapScreen = () => { attributionPosition={{right: 8, bottom: 8}} compassEnabled={false} scaleBarEnabled={false} - styleURL={MAP_STYLE} + styleURL={styleUrlQuery.data} onDidFinishLoadingStyle={handleDidFinishLoadingStyle} onMoveShouldSetResponder={() => { if (following) setFollowing(false); diff --git a/src/frontend/screens/Observation/InsetMapView.tsx b/src/frontend/screens/Observation/InsetMapView.tsx index 55bb17fe4..55379a8c7 100644 --- a/src/frontend/screens/Observation/InsetMapView.tsx +++ b/src/frontend/screens/Observation/InsetMapView.tsx @@ -4,7 +4,7 @@ import {View, Text, StyleSheet, Dimensions, Image} from 'react-native'; import {BLACK, WHITE} from '../../lib/styles'; import {usePersistedSettings} from '../../hooks/persistedState/usePersistedSettings'; import {FormattedCoords} from '../../sharedComponents/FormattedData'; -import {MAP_STYLE} from '../MapScreen'; +import {useMapStyleUrl} from '../../hooks/server/mapStyleUrl'; const MAP_HEIGHT = 175; const ICON_OFFSET = {x: 22, y: 21}; @@ -16,6 +16,8 @@ type MapProps = { export const InsetMapView = React.memo(({lon, lat}: MapProps) => { const format = usePersistedSettings(store => store.coordinateFormat); + const styleUrlQuery = useMapStyleUrl(); + return ( (({lon, lat}: MapProps) => { rotateEnabled={false} compassEnabled={false} scaleBarEnabled={false} - styleURL={MAP_STYLE}> + styleURL={styleUrlQuery.data}>