fix(maps): dedupe map sources by region so duplicate files don't blank the map
The map style names each source by its date-stripped region (both "washington.pmtiles" and "washington_2025-12.pmtiles" -> "washington"). When an old and new copy of the same region are both on disk, the style emitted two sources with the same key and duplicate layer ids, which MapLibre rejects outright -- blanking the ENTIRE map, not just that region. Old copies linger when a newer curated version installs (#634), so a user who updates maps can silently lose all map rendering until the stale file is removed by hand. generateSourcesArray() now keeps only the newest file per region: a dated build beats an undated legacy file, and between two dated builds the later YYYY-MM wins. The skipped duplicate is logged. The style stays valid even when stale files are present. Complements #981, which removes superseded curated files on install. This is the runtime safety net that also recovers installs already in the broken state (which a cleanup-on-install alone can't reach). Refs #634
This commit is contained in:
parent
a315ce0f54
commit
9b84d3aa54
|
|
@ -457,6 +457,18 @@ export class MapService implements IMapService {
|
|||
return await listDirectoryContentsRecursive(this.baseDirPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two map-file versions (YYYY-MM, or null for an undated legacy file). Returns >0 if
|
||||
* `a` is newer than `b`, <0 if older, 0 if equal. A dated build is always newer than an undated
|
||||
* legacy file; two dated builds compare lexicographically (correct for zero-padded YYYY-MM).
|
||||
*/
|
||||
private static compareMapVersions(a: string | null, b: string | null): number {
|
||||
if (a === b) return 0
|
||||
if (a === null) return -1
|
||||
if (b === null) return 1
|
||||
return a < b ? -1 : 1
|
||||
}
|
||||
|
||||
private generateSourcesArray(host: string | null, regions: FileEntry[], protocol: string = 'http'): BaseStylesFile['sources'][] {
|
||||
const sources: BaseStylesFile['sources'][] = []
|
||||
const baseUrl = this.getPublicFileBaseUrl(host, 'pmtiles', protocol)
|
||||
|
|
@ -474,23 +486,47 @@ export class MapService implements IMapService {
|
|||
sources.push(worldSource)
|
||||
}
|
||||
|
||||
// Dedupe by region name, keeping only the newest file per region. The source name is the
|
||||
// date-stripped region (e.g. both "washington.pmtiles" and "washington_2025-12.pmtiles" map
|
||||
// to "washington"). Emitting both produces duplicate source keys and duplicate layer ids,
|
||||
// which MapLibre rejects outright — blanking the ENTIRE map, not just that region. Old copies
|
||||
// linger when a newer curated version installs (#634), so guard against it here so the style
|
||||
// stays valid even if cleanup hasn't run. A dated build beats an undated legacy file; between
|
||||
// two dated builds the later YYYY-MM wins (lexicographic compare is correct for that format).
|
||||
const bestByRegion = new Map<string, { region: FileEntry; version: string | null }>()
|
||||
for (const region of regions) {
|
||||
if (region.type === 'file' && region.name.endsWith('.pmtiles')) {
|
||||
// Strip .pmtiles and date suffix (e.g. "alaska_2025-12" -> "alaska") for stable source names
|
||||
const parsed = CollectionManifestService.parseMapFilename(region.name)
|
||||
const regionName = parsed ? parsed.resource_id : region.name.replace('.pmtiles', '')
|
||||
const source: BaseStylesFile['sources'] = {}
|
||||
const sourceUrl = urlJoin(baseUrl, region.name)
|
||||
|
||||
source[regionName] = {
|
||||
type: 'vector',
|
||||
attribution: PMTILES_ATTRIBUTION,
|
||||
url: `pmtiles://${sourceUrl}`,
|
||||
const version = parsed?.version ?? null
|
||||
const existing = bestByRegion.get(regionName)
|
||||
if (!existing || MapService.compareMapVersions(version, existing.version) > 0) {
|
||||
if (existing) {
|
||||
logger.warn(
|
||||
`[MapService] Duplicate map region "${regionName}": using "${region.name}" over "${existing.region.name}" (keeping newest)`
|
||||
)
|
||||
}
|
||||
bestByRegion.set(regionName, { region, version })
|
||||
} else {
|
||||
logger.warn(
|
||||
`[MapService] Duplicate map region "${regionName}": skipping "${region.name}" in favor of "${existing.region.name}" (keeping newest)`
|
||||
)
|
||||
}
|
||||
sources.push(source)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [regionName, { region }] of bestByRegion) {
|
||||
const source: BaseStylesFile['sources'] = {}
|
||||
const sourceUrl = urlJoin(baseUrl, region.name)
|
||||
|
||||
source[regionName] = {
|
||||
type: 'vector',
|
||||
attribution: PMTILES_ATTRIBUTION,
|
||||
url: `pmtiles://${sourceUrl}`,
|
||||
}
|
||||
sources.push(source)
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue