feat(map): amap as map api provider and display template

This commit is contained in:
A1phaN
2026-09-07 10:04:12 +08:00
committed by Maurice
parent a066c94ec5
commit 57eac32f9f
92 changed files with 2910 additions and 70 deletions
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest'
import { effectivePlacesProvider, osmKeySuggestion } from './placesProvider'
/**
* These mirror MapsService.resolvePlacesProvider() on the server, case for case.
* The point of the mirror is honest notices, so every case where the notices
* would lie — "add a Google key" on an install whose admin picked Amap — is
* pinned here.
*/
describe('effectivePlacesProvider', () => {
it('auto keeps Google when a Google key is configured, Amap key or not', () => {
// The incumbent wins: an existing install must not move providers because
// a new one became available.
expect(effectivePlacesProvider({ placesProvider: 'auto', hasMapsKey: true, hasAmapKey: true })).toBe('google')
expect(effectivePlacesProvider({ placesProvider: 'auto', hasMapsKey: true, hasAmapKey: false })).toBe('google')
})
it('auto falls to Amap only when there is no Google key', () => {
expect(effectivePlacesProvider({ placesProvider: 'auto', hasMapsKey: false, hasAmapKey: true })).toBe('amap')
})
it('auto with neither key means OpenStreetMap', () => {
expect(effectivePlacesProvider({ placesProvider: 'auto', hasMapsKey: false, hasAmapKey: false })).toBe('openstreetmap')
})
it('an explicit amap choice wins even with a Google key configured', () => {
expect(effectivePlacesProvider({ placesProvider: 'amap', hasMapsKey: true, hasAmapKey: true })).toBe('amap')
})
it('an explicit google choice never silently uses Amap instead', () => {
// Misconfigured means "answer with OSM", matching the server — not "bill
// somebody else's provider".
expect(effectivePlacesProvider({ placesProvider: 'google', hasMapsKey: false, hasAmapKey: true })).toBe('openstreetmap')
})
it('openstreetmap ignores both keys', () => {
expect(effectivePlacesProvider({ placesProvider: 'openstreetmap', hasMapsKey: true, hasAmapKey: true })).toBe('openstreetmap')
})
it('a nonsense stored value degrades to auto rather than throwing', () => {
expect(effectivePlacesProvider({ placesProvider: 'not-a-provider', hasMapsKey: false, hasAmapKey: true })).toBe('amap')
expect(effectivePlacesProvider({ placesProvider: undefined, hasMapsKey: true, hasAmapKey: false })).toBe('google')
})
})
describe('osmKeySuggestion', () => {
it('a chosen provider names its own key', () => {
expect(osmKeySuggestion('google')).toBe('google')
expect(osmKeySuggestion('amap')).toBe('amap')
})
it('auto names both, because landing on OSM means neither key is set', () => {
expect(osmKeySuggestion('auto')).toBe('any')
})
it('an explicit OpenStreetMap choice gets no upsell — it is a decision, not a gap', () => {
expect(osmKeySuggestion('openstreetmap')).toBeNull()
// An unrecognised value behaves like auto: the reader degrades it to auto,
// so the notice must not claim the admin chose OSM when they chose nothing.
expect(osmKeySuggestion('garbage')).toBe('any')
expect(osmKeySuggestion(undefined)).toBe('any')
})
})
+69
View File
@@ -0,0 +1,69 @@
/**
* Which provider place search will actually use, as far as the client can tell.
*
* This mirrors the server's MapsService.resolvePlacesProvider(): the instance's
* places_provider choice, the keys the server reported for this caller, and the
* same precedence. It exists so a notice can say the truth — "search is running
* on OpenStreetMap", not "you lack a Google key" on an install whose admin
* deliberately picked Amap.
*
* A mirror and not the source of truth on purpose: the server resolves keys per
* request against the database, and only it knows for sure. If the two ever
* disagree, the client shows a slightly wrong hint under a notice and nothing
* else — search itself keeps asking the server.
*/
export type PlacesProviderChoice = 'auto' | 'google' | 'amap' | 'openstreetmap'
export type EffectiveSearchProvider = 'google' | 'amap' | 'openstreetmap'
export interface PlacesProviderState {
/** The admin's places_provider choice; anything unrecognised means 'auto'. */
placesProvider?: string | null
/** Whether a Google key resolves for this caller (operator env, instance, own row). */
hasMapsKey: boolean
hasAmapKey: boolean
}
export function effectivePlacesProvider(state: PlacesProviderState): EffectiveSearchProvider {
const choice: PlacesProviderChoice =
state.placesProvider === 'google' || state.placesProvider === 'amap' || state.placesProvider === 'openstreetmap'
? state.placesProvider
: 'auto'
if (choice === 'openstreetmap') return 'openstreetmap'
if (choice === 'google') return state.hasMapsKey ? 'google' : 'openstreetmap'
if (choice === 'amap') return state.hasAmapKey ? 'amap' : 'openstreetmap'
// auto prefers Google, the incumbent: an existing install must not move to a
// different provider, with a different bill, just because Amap appeared.
if (state.hasMapsKey) return 'google'
if (state.hasAmapKey) return 'amap'
return 'openstreetmap'
}
/**
* Which key the "still on OpenStreetMap" upsell should suggest.
*
* A chosen provider names the key that belongs to it — telling an admin who
* picked Amap to add a Google key is advice they already declined. `auto` with
* no key at all is the only case where both are worth naming.
*
* Null when the admin explicitly chose OpenStreetMap: that is a decision, not a
* gap to fill, and the notices stay quiet rather than nag.
*/
export type KeySuggestion = 'google' | 'amap' | 'any'
export function osmKeySuggestion(placesProvider?: string | null): KeySuggestion | null {
switch (placesProvider) {
case 'google':
return 'google'
case 'amap':
return 'amap'
case 'openstreetmap':
return null
// auto resolving to OSM means neither key is configured (a Google key would
// have won), so both are honest suggestions.
default:
return 'any'
}
}
+19
View File
@@ -97,6 +97,25 @@ export function isVectorStyle(url: string | null | undefined): boolean {
return /^https?:\/\//i.test(u) || u.startsWith('mapbox://')
}
/**
* Tiles drawn in GCJ-02 rather than WGS-84.
*
* Amap serves its raster tiles from `*.is.autonavi.com`, and they are shifted
* from the WGS-84 coordinates everything else in TREK uses — by 100 to 800 m
* depending on where in China you are. A map on one of these has to project
* through components/Map/gcj02Crs.ts or every marker lands on the wrong street.
*
* Matched on the host, not on the preset constants, so a self-hosted proxy in
* front of the same tiles still gets the right CRS as long as the hostname
* survives — and so a template a user typed in by hand does too.
*/
const GCJ02_TILE_HOST = /(^|\.)is\.autonavi\.com$/i
export function isGcj02Basemap(url: string | null | undefined): boolean {
if (!url) return false
return GCJ02_TILE_HOST.test(templateHost(url))
}
/**
* What a map should draw, given the user's template and the app's default.
*