fix(maps): warn when world basemap missing instead of silent grey map
An install that never went online while visiting /maps never provisions world.pmtiles (it is lazily extracted from the internet), leaving the map blank/grey offline outside any downloaded regional extracts. Detect the world basemap's presence on disk and surface it to the UI so: - /maps shows a clear notice explaining the base map isn't downloaded and how to fix it, instead of a silent grey screen. - /settings/maps offers an explicit "Download Base Map" action (~15 MB) to provision it deliberately while online. Adds MapService.checkWorldBasemapExists() / provisionWorldBasemap(), a POST /maps/setup-world-basemap endpoint, and threads worldBasemapExists through the maps page props. Refs #1030 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6a4f02dd46
commit
d13679f961
|
|
@ -19,10 +19,14 @@ export default class MapsController {
|
|||
|
||||
async index({ inertia }: HttpContext) {
|
||||
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', {
|
||||
maps: {
|
||||
baseAssetsExist: baseAssetsCheck,
|
||||
worldBasemapExists,
|
||||
regionFiles: regionFiles.files,
|
||||
},
|
||||
})
|
||||
|
|
@ -35,6 +39,24 @@ export default class MapsController {
|
|||
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) {
|
||||
const payload = await request.validateUsing(remoteDownloadValidator)
|
||||
assertNotPrivateUrl(payload.url)
|
||||
|
|
|
|||
|
|
@ -45,10 +45,14 @@ export default class SettingsController {
|
|||
|
||||
async maps({ inertia }: HttpContext) {
|
||||
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', {
|
||||
maps: {
|
||||
baseAssetsExist: baseAssetsCheck,
|
||||
worldBasemapExists,
|
||||
regionFiles: regionFiles.files,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -404,6 +404,34 @@ export class MapService implements IMapService {
|
|||
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
|
||||
* 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<{
|
||||
message: string
|
||||
slug: string
|
||||
|
|
|
|||
|
|
@ -10,13 +10,15 @@ import Alert from '~/components/Alert'
|
|||
import { FileEntry } from '../../types/files'
|
||||
|
||||
export default function Maps(props: {
|
||||
maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] }
|
||||
maps: { baseAssetsExist: boolean; worldBasemapExists: boolean; regionFiles: FileEntry[] }
|
||||
}) {
|
||||
const [isHoveringUI, setIsHoveringUI] = useState(false)
|
||||
const [showMapCoordinates, setShowMapCoordinates] = useState(true)
|
||||
|
||||
const alertMessage = !props.maps.baseAssetsExist
|
||||
? '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
|
||||
? 'No map regions have been downloaded yet. Please download some regions to enable map functionality.'
|
||||
: null
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const CURATED_COLLECTIONS_KEY = 'curated-map-collections'
|
|||
const GLOBAL_MAP_INFO_KEY = 'global-map-info'
|
||||
|
||||
export default function MapsManager(props: {
|
||||
maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] }
|
||||
maps: { baseAssetsExist: boolean; worldBasemapExists: boolean; regionFiles: FileEntry[] }
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const { openModal, closeAllModals } = useModals()
|
||||
|
|
@ -61,6 +61,24 @@ export default function MapsManager(props: {
|
|||
})
|
||||
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({
|
||||
mutationFn: () => api.downloadGlobalMap(),
|
||||
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 && (
|
||||
<Alert
|
||||
title="Global Map Installed"
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ router
|
|||
router.get('/curated-collections', [MapsController, 'listCuratedCollections'])
|
||||
router.post('/fetch-latest-collections', [MapsController, 'fetchLatestCollections'])
|
||||
router.post('/download-base-assets', [MapsController, 'downloadBaseAssets'])
|
||||
router.post('/setup-world-basemap', [MapsController, 'setupWorldBasemap'])
|
||||
router.post('/download-remote', [MapsController, 'downloadRemote'])
|
||||
router.post('/download-remote-preflight', [MapsController, 'downloadRemotePreflight'])
|
||||
router.post('/download-collection', [MapsController, 'downloadCollection'])
|
||||
|
|
|
|||
Loading…
Reference in New Issue