Merge d13679f961 into 3ab916bf75
This commit is contained in:
commit
329fa59e15
|
|
@ -19,10 +19,14 @@ export default class MapsController {
|
||||||
|
|
||||||
async index({ inertia }: HttpContext) {
|
async index({ inertia }: HttpContext) {
|
||||||
const baseAssetsCheck = await this.mapService.ensureBaseAssets()
|
const baseAssetsCheck = await this.mapService.ensureBaseAssets()
|
||||||
const regionFiles = await this.mapService.listRegions()
|
const [regionFiles, worldBasemapExists] = await Promise.all([
|
||||||
|
this.mapService.listRegions(),
|
||||||
|
this.mapService.checkWorldBasemapExists(),
|
||||||
|
])
|
||||||
return inertia.render('maps', {
|
return inertia.render('maps', {
|
||||||
maps: {
|
maps: {
|
||||||
baseAssetsExist: baseAssetsCheck,
|
baseAssetsExist: baseAssetsCheck,
|
||||||
|
worldBasemapExists,
|
||||||
regionFiles: regionFiles.files,
|
regionFiles: regionFiles.files,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -35,6 +39,24 @@ export default class MapsController {
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setupWorldBasemap({ response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const ready = await this.mapService.provisionWorldBasemap()
|
||||||
|
if (!ready) {
|
||||||
|
return response.status(500).send({
|
||||||
|
message:
|
||||||
|
'Could not download the base map. Please connect this NOMAD to the internet and try again.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { success: true }
|
||||||
|
} catch {
|
||||||
|
return response.status(500).send({
|
||||||
|
message:
|
||||||
|
'Could not download the base map. Please connect this NOMAD to the internet and try again.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async downloadRemote({ request }: HttpContext) {
|
async downloadRemote({ request }: HttpContext) {
|
||||||
const payload = await request.validateUsing(remoteDownloadValidator)
|
const payload = await request.validateUsing(remoteDownloadValidator)
|
||||||
assertNotPrivateUrl(payload.url)
|
assertNotPrivateUrl(payload.url)
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,14 @@ export default class SettingsController {
|
||||||
|
|
||||||
async maps({ inertia }: HttpContext) {
|
async maps({ inertia }: HttpContext) {
|
||||||
const baseAssetsCheck = await this.mapService.ensureBaseAssets()
|
const baseAssetsCheck = await this.mapService.ensureBaseAssets()
|
||||||
const regionFiles = await this.mapService.listRegions()
|
const [regionFiles, worldBasemapExists] = await Promise.all([
|
||||||
|
this.mapService.listRegions(),
|
||||||
|
this.mapService.checkWorldBasemapExists(),
|
||||||
|
])
|
||||||
return inertia.render('settings/maps', {
|
return inertia.render('settings/maps', {
|
||||||
maps: {
|
maps: {
|
||||||
baseAssetsExist: baseAssetsCheck,
|
baseAssetsExist: baseAssetsCheck,
|
||||||
|
worldBasemapExists,
|
||||||
regionFiles: regionFiles.files,
|
regionFiles: regionFiles.files,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,34 @@ export class MapService implements IMapService {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the low-zoom world basemap is present on disk. Checked directly (rather than trusting
|
||||||
|
* the in-process `worldBasemapReady` flag) so callers get an accurate answer regardless of whether
|
||||||
|
* `ensureWorldBasemap()` has run yet this process. Used to warn the user instead of showing a
|
||||||
|
* silent grey map when the basemap was never provisioned (e.g. installed straight offline, #1030).
|
||||||
|
*/
|
||||||
|
async checkWorldBasemapExists(): Promise<boolean> {
|
||||||
|
const basePath = resolve(join(this.baseDirPath, 'pmtiles'))
|
||||||
|
const filepath = resolve(join(basePath, WORLD_BASEMAP_FILENAME))
|
||||||
|
if (!filepath.startsWith(basePath + sep)) return false
|
||||||
|
const stats = await getFileStatsIfExists(filepath)
|
||||||
|
const exists = !!stats && Number(stats.size) > 0
|
||||||
|
if (exists) this.worldBasemapReady = true
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly (re)provision the world basemap on demand. Ensures base assets exist, then extracts
|
||||||
|
* the basemap if it isn't present yet. Requires internet — surfaced to the user as a "download the
|
||||||
|
* base map" action so the one network dependency can be satisfied deliberately while online (#1030).
|
||||||
|
*/
|
||||||
|
async provisionWorldBasemap(): Promise<boolean> {
|
||||||
|
const baseAssetsExist = await this.ensureBaseAssets()
|
||||||
|
if (!baseAssetsExist) return false
|
||||||
|
await this.ensureWorldBasemap()
|
||||||
|
return this.checkWorldBasemapExists()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract a low-zoom global basemap once so the map isn't grey outside a
|
* Extract a low-zoom global basemap once so the map isn't grey outside a
|
||||||
* regional extract's polygon. Cheap (~15 MB, a handful of HTTP range
|
* regional extract's polygon. Cheap (~15 MB, a handful of HTTP range
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ class API {
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setupWorldBasemap() {
|
||||||
|
return catchInternal(async () => {
|
||||||
|
const response = await this.client.post<{ success: boolean }>('/maps/setup-world-basemap')
|
||||||
|
return response.data
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
async downloadMapCollection(slug: string): Promise<{
|
async downloadMapCollection(slug: string): Promise<{
|
||||||
message: string
|
message: string
|
||||||
slug: string
|
slug: string
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,15 @@ import Alert from '~/components/Alert'
|
||||||
import { FileEntry } from '../../types/files'
|
import { FileEntry } from '../../types/files'
|
||||||
|
|
||||||
export default function Maps(props: {
|
export default function Maps(props: {
|
||||||
maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] }
|
maps: { baseAssetsExist: boolean; worldBasemapExists: boolean; regionFiles: FileEntry[] }
|
||||||
}) {
|
}) {
|
||||||
const [isHoveringUI, setIsHoveringUI] = useState(false)
|
const [isHoveringUI, setIsHoveringUI] = useState(false)
|
||||||
const [showMapCoordinates, setShowMapCoordinates] = useState(true)
|
const [showMapCoordinates, setShowMapCoordinates] = useState(true)
|
||||||
|
|
||||||
const alertMessage = !props.maps.baseAssetsExist
|
const alertMessage = !props.maps.baseAssetsExist
|
||||||
? 'The base map assets have not been installed. Please download them first to enable map functionality.'
|
? 'The base map assets have not been installed. Please download them first to enable map functionality.'
|
||||||
|
: !props.maps.worldBasemapExists
|
||||||
|
? 'The world base map has not been downloaded yet, so the map may appear blank outside downloaded regions. Connect this NOMAD to the internet and download it (~15 MB) from Map Settings.'
|
||||||
: props.maps.regionFiles.length === 0
|
: props.maps.regionFiles.length === 0
|
||||||
? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.'
|
? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.'
|
||||||
: null
|
: null
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const CURATED_COLLECTIONS_KEY = 'curated-map-collections'
|
||||||
const GLOBAL_MAP_INFO_KEY = 'global-map-info'
|
const GLOBAL_MAP_INFO_KEY = 'global-map-info'
|
||||||
|
|
||||||
export default function MapsManager(props: {
|
export default function MapsManager(props: {
|
||||||
maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] }
|
maps: { baseAssetsExist: boolean; worldBasemapExists: boolean; regionFiles: FileEntry[] }
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, closeAllModals } = useModals()
|
const { openModal, closeAllModals } = useModals()
|
||||||
|
|
@ -61,6 +61,24 @@ export default function MapsManager(props: {
|
||||||
})
|
})
|
||||||
const globalMapAlreadyDownloaded = hasDownloadedGlobalMap(globalMapInfo?.key, props.maps.regionFiles)
|
const globalMapAlreadyDownloaded = hasDownloadedGlobalMap(globalMapInfo?.key, props.maps.regionFiles)
|
||||||
|
|
||||||
|
const setupWorldBasemap = useMutation({
|
||||||
|
mutationFn: () => api.setupWorldBasemap(),
|
||||||
|
onSuccess: () => {
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Base map downloaded successfully.',
|
||||||
|
})
|
||||||
|
router.reload({ only: ['maps'] })
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Could not download the base map. Please connect this NOMAD to the internet and try again.',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const downloadGlobalMap = useMutation({
|
const downloadGlobalMap = useMutation({
|
||||||
mutationFn: () => api.downloadGlobalMap(),
|
mutationFn: () => api.downloadGlobalMap(),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -306,6 +324,22 @@ export default function MapsManager(props: {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{props.maps.baseAssetsExist && !props.maps.worldBasemapExists && (
|
||||||
|
<Alert
|
||||||
|
title="World base map not downloaded"
|
||||||
|
message="The low-zoom world base map (~15 MB) hasn't been downloaded yet, so the map appears blank outside any regions you've downloaded. Connect this NOMAD to the internet and download it once for offline use."
|
||||||
|
type="warning"
|
||||||
|
variant="solid"
|
||||||
|
className="my-4"
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'secondary',
|
||||||
|
children: 'Download Base Map',
|
||||||
|
icon: 'IconCloudDownload',
|
||||||
|
loading: setupWorldBasemap.isPending,
|
||||||
|
onClick: () => setupWorldBasemap.mutate(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{globalMapInfo && globalMapAlreadyDownloaded && (
|
{globalMapInfo && globalMapAlreadyDownloaded && (
|
||||||
<Alert
|
<Alert
|
||||||
title="Global Map Installed"
|
title="Global Map Installed"
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ router
|
||||||
router.get('/curated-collections', [MapsController, 'listCuratedCollections'])
|
router.get('/curated-collections', [MapsController, 'listCuratedCollections'])
|
||||||
router.post('/fetch-latest-collections', [MapsController, 'fetchLatestCollections'])
|
router.post('/fetch-latest-collections', [MapsController, 'fetchLatestCollections'])
|
||||||
router.post('/download-base-assets', [MapsController, 'downloadBaseAssets'])
|
router.post('/download-base-assets', [MapsController, 'downloadBaseAssets'])
|
||||||
|
router.post('/setup-world-basemap', [MapsController, 'setupWorldBasemap'])
|
||||||
router.post('/download-remote', [MapsController, 'downloadRemote'])
|
router.post('/download-remote', [MapsController, 'downloadRemote'])
|
||||||
router.post('/download-remote-preflight', [MapsController, 'downloadRemotePreflight'])
|
router.post('/download-remote-preflight', [MapsController, 'downloadRemotePreflight'])
|
||||||
router.post('/download-collection', [MapsController, 'downloadCollection'])
|
router.post('/download-collection', [MapsController, 'downloadCollection'])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue