reafactor(important): change to new map backround base goong.io
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
setStoredTokens,
|
setStoredTokens,
|
||||||
} from "@/auth/tokenStore"
|
} from "@/auth/tokenStore"
|
||||||
|
|
||||||
const baseURL = API_URL_ROOT || "https://history-api.kain.id.vn"
|
const baseURL = API_URL_ROOT
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
|
|||||||
+46
-9
@@ -1,9 +1,50 @@
|
|||||||
// Production BackEndGo API base URL.
|
import { API_URL_ROOT } from "../../../api";
|
||||||
// For local development, override with NEXT_PUBLIC_API_BASE_URL (e.g. http://localhost:3344).
|
|
||||||
const FALLBACK_API_BASE_URL = "https://history-api.kain.id.vn";
|
|
||||||
|
|
||||||
export const API_BASE_URL =
|
const GOONG_TILES_BASE_URL = "https://tiles.goong.io";
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL || FALLBACK_API_BASE_URL;
|
|
||||||
|
export const API_BASE_URL = normalizeApiBaseUrl(API_URL_ROOT);
|
||||||
|
const GOONG_PROXY_BASE_PATH = `${API_BASE_URL}/proxy`;
|
||||||
|
|
||||||
|
export const GOONG_SATELLITE_STYLE_UPSTREAM_URL = `${GOONG_TILES_BASE_URL}/assets/goong_satellite.json`;
|
||||||
|
export const GOONG_VECTOR_OVERLAY_STYLE_UPSTREAM_URL = `${GOONG_TILES_BASE_URL}/assets/goong_map_web.json`;
|
||||||
|
export const GOONG_GLYPHS_UPSTREAM_URL = `${GOONG_TILES_BASE_URL}/fonts/{fontstack}/{range}.pbf`;
|
||||||
|
|
||||||
|
export const USE_EXTERNAL_BACKGROUND_RASTER = API_BASE_URL.length > 0;
|
||||||
|
|
||||||
|
function normalizeApiBaseUrl(rawUrl: string): string {
|
||||||
|
return rawUrl.trim().replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripGoongApiKeyFromUrl(rawUrl: string): string {
|
||||||
|
const [basePart, hashPart = ""] = rawUrl.split("#", 2);
|
||||||
|
const [pathPart, queryString = ""] = basePart.split("?", 2);
|
||||||
|
const sanitizedQuery = queryString
|
||||||
|
.split("&")
|
||||||
|
.filter((segment) => segment && !segment.toLowerCase().startsWith("api_key="))
|
||||||
|
.join("&");
|
||||||
|
|
||||||
|
return `${pathPart}${sanitizedQuery ? `?${sanitizedQuery}` : ""}${hashPart ? `#${hashPart}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGoongProxyUrl(rawUrl: string): string {
|
||||||
|
const sanitizedUrl = stripGoongApiKeyFromUrl(rawUrl);
|
||||||
|
const templateTokens: string[] = [];
|
||||||
|
const tokenizedUrl = sanitizedUrl.replace(/\{[^}]+\}/g, (match) => {
|
||||||
|
const tokenId = `__UHM_GOONG_URL_TOKEN_${templateTokens.length}__`;
|
||||||
|
templateTokens.push(match);
|
||||||
|
return tokenId;
|
||||||
|
});
|
||||||
|
|
||||||
|
let encodedUrl = encodeURIComponent(tokenizedUrl);
|
||||||
|
templateTokens.forEach((token, index) => {
|
||||||
|
const encodedTokenId = encodeURIComponent(`__UHM_GOONG_URL_TOKEN_${index}__`);
|
||||||
|
encodedUrl = encodedUrl.replace(encodedTokenId, token);
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${GOONG_PROXY_BASE_PATH}/${encodedUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GOONG_GLYPHS_PROXY_URL = buildGoongProxyUrl(GOONG_GLYPHS_UPSTREAM_URL);
|
||||||
|
|
||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
geometries: `${API_BASE_URL}/geometries`,
|
geometries: `${API_BASE_URL}/geometries`,
|
||||||
@@ -18,8 +59,4 @@ export const API_ENDPOINTS = {
|
|||||||
currentUserProjects: `${API_BASE_URL}/users/current/project`,
|
currentUserProjects: `${API_BASE_URL}/users/current/project`,
|
||||||
projects: `${API_BASE_URL}/projects`,
|
projects: `${API_BASE_URL}/projects`,
|
||||||
submissions: `${API_BASE_URL}/submissions`,
|
submissions: `${API_BASE_URL}/submissions`,
|
||||||
vectorTiles: `${API_BASE_URL}/tiles/{z}/{x}/{y}`,
|
|
||||||
rasterTiles: `${API_BASE_URL}/raster-tiles/{z}/{x}/{y}`,
|
|
||||||
vectorTilesMetadata: `${API_BASE_URL}/tiles/metadata`,
|
|
||||||
rasterTilesMetadata: `${API_BASE_URL}/raster-tiles/metadata`,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+575
-11
@@ -1,20 +1,584 @@
|
|||||||
import { API_ENDPOINTS } from "@/uhm/api/config";
|
import {
|
||||||
import { requestJson } from "@/uhm/api/http";
|
buildGoongProxyUrl,
|
||||||
|
GOONG_SATELLITE_STYLE_UPSTREAM_URL,
|
||||||
|
GOONG_VECTOR_OVERLAY_STYLE_UPSTREAM_URL,
|
||||||
|
USE_EXTERNAL_BACKGROUND_RASTER,
|
||||||
|
} from "@/uhm/api/config";
|
||||||
|
import { GOONG_LABEL_FALLBACK_FONT_STACK } from "@/uhm/lib/map/styles/shared/textFonts";
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
|
||||||
export type TileMetadata = Record<string, string>;
|
export type GoongBackgroundGroupId =
|
||||||
|
| "bg-country-borders-line"
|
||||||
|
| "bg-province-borders-line"
|
||||||
|
| "bg-district-borders-line"
|
||||||
|
| "country-labels"
|
||||||
|
| "rivers-line";
|
||||||
|
|
||||||
export function getVectorTileTemplateUrl(): string {
|
type GoongStyleSource = {
|
||||||
return API_ENDPOINTS.vectorTiles;
|
type?: string;
|
||||||
|
url?: string;
|
||||||
|
tiles?: string[];
|
||||||
|
tileSize?: number;
|
||||||
|
attribution?: string;
|
||||||
|
bounds?: number[];
|
||||||
|
scheme?: "xyz" | "tms";
|
||||||
|
minzoom?: number;
|
||||||
|
maxzoom?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GoongSourceManifest = {
|
||||||
|
tiles?: string[];
|
||||||
|
tileSize?: number;
|
||||||
|
pixel_scale?: number | string;
|
||||||
|
attribution?: string;
|
||||||
|
bounds?: number[];
|
||||||
|
scheme?: "xyz" | "tms";
|
||||||
|
minzoom?: number;
|
||||||
|
maxzoom?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GoongStyleDocument = {
|
||||||
|
glyphs?: string;
|
||||||
|
sprite?: string;
|
||||||
|
sources?: Record<string, GoongStyleSource>;
|
||||||
|
layers?: maplibregl.LayerSpecification[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let externalRasterSourcePromise: Promise<maplibregl.RasterSourceSpecification> | null = null;
|
||||||
|
let goongOverlayBundlePromise: Promise<GoongBackgroundOverlayBundle | null> | null = null;
|
||||||
|
const goongStyleDocumentPromises = new Map<string, Promise<GoongStyleDocument>>();
|
||||||
|
const goongSourceSpecificationPromises = new Map<string, Promise<maplibregl.SourceSpecification>>();
|
||||||
|
|
||||||
|
type GoongBackgroundOverlayBundle = {
|
||||||
|
sources: Record<string, maplibregl.SourceSpecification>;
|
||||||
|
layers: maplibregl.LayerSpecification[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBackgroundRasterSourceSpecification(): Promise<maplibregl.RasterSourceSpecification> {
|
||||||
|
if (!USE_EXTERNAL_BACKGROUND_RASTER) {
|
||||||
|
throw new Error("NEXT_PUBLIC_API_URL_ROOT is not configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRasterTileTemplateUrl(): string {
|
if (!externalRasterSourcePromise) {
|
||||||
return API_ENDPOINTS.rasterTiles;
|
externalRasterSourcePromise = loadGoongRasterSourceSpecification(
|
||||||
|
GOONG_SATELLITE_STYLE_UPSTREAM_URL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchVectorTilesMetadata(): Promise<TileMetadata> {
|
try {
|
||||||
return requestJson<TileMetadata>(API_ENDPOINTS.vectorTilesMetadata);
|
return await externalRasterSourcePromise;
|
||||||
|
} catch (error) {
|
||||||
|
externalRasterSourcePromise = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchRasterTilesMetadata(): Promise<TileMetadata> {
|
export async function getGoongBackgroundOverlayBundle(): Promise<GoongBackgroundOverlayBundle | null> {
|
||||||
return requestJson<TileMetadata>(API_ENDPOINTS.rasterTilesMetadata);
|
if (!USE_EXTERNAL_BACKGROUND_RASTER) {
|
||||||
|
throw new Error("NEXT_PUBLIC_API_URL_ROOT is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!goongOverlayBundlePromise) {
|
||||||
|
goongOverlayBundlePromise = loadGoongBackgroundOverlayBundle(
|
||||||
|
GOONG_VECTOR_OVERLAY_STYLE_UPSTREAM_URL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await goongOverlayBundlePromise;
|
||||||
|
} catch (error) {
|
||||||
|
goongOverlayBundlePromise = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGoongRasterSourceSpecification(
|
||||||
|
styleUpstreamUrl: string
|
||||||
|
): Promise<maplibregl.RasterSourceSpecification> {
|
||||||
|
const style = await loadGoongStyleDocument(styleUpstreamUrl);
|
||||||
|
const sources = style.sources || {};
|
||||||
|
|
||||||
|
for (const source of Object.values(sources)) {
|
||||||
|
if (source.type !== "raster") continue;
|
||||||
|
|
||||||
|
const spec = await normalizeGoongSourceSpecification(source, styleUpstreamUrl);
|
||||||
|
if (spec.type === "raster" && (spec.tiles?.length || "url" in spec)) {
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No raster source found in Goong satellite style.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGoongBackgroundOverlayBundle(
|
||||||
|
styleUpstreamUrl: string
|
||||||
|
): Promise<GoongBackgroundOverlayBundle | null> {
|
||||||
|
const style = await loadGoongStyleDocument(styleUpstreamUrl);
|
||||||
|
const layers = style.layers || [];
|
||||||
|
const sources = style.sources || {};
|
||||||
|
const layerById = new Map(layers.map((layer) => [layer.id, layer]));
|
||||||
|
const selectedLayersByGroup = new Map<GoongBackgroundGroupId, maplibregl.LayerSpecification[]>([
|
||||||
|
["bg-country-borders-line", []],
|
||||||
|
["bg-province-borders-line", []],
|
||||||
|
["bg-district-borders-line", []],
|
||||||
|
["rivers-line", []],
|
||||||
|
["country-labels", []],
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const rawLayer of layers) {
|
||||||
|
const resolvedLayer = resolveLayerReference(rawLayer, layerById);
|
||||||
|
const groupId = detectGoongBackgroundGroup(resolvedLayer);
|
||||||
|
if (!groupId) continue;
|
||||||
|
selectedLayersByGroup.get(groupId)?.push(resolvedLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSourceIds = new Set<string>();
|
||||||
|
for (const groupLayers of selectedLayersByGroup.values()) {
|
||||||
|
for (const layer of groupLayers) {
|
||||||
|
if ("source" in layer && typeof layer.source === "string") {
|
||||||
|
selectedSourceIds.add(layer.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSourceIds.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceIdMap = new Map<string, string>();
|
||||||
|
const overlaySources: Record<string, maplibregl.SourceSpecification> = {};
|
||||||
|
const overlaySourceEntries = await Promise.all(
|
||||||
|
[...selectedSourceIds].map(async (sourceId) => {
|
||||||
|
const source = sources[sourceId];
|
||||||
|
if (!source) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefixedId = `goong-overlay-${sourceId}`;
|
||||||
|
const normalizedSource = await normalizeGoongSourceSpecification(
|
||||||
|
source,
|
||||||
|
styleUpstreamUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
return { sourceId, prefixedId, normalizedSource };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const entry of overlaySourceEntries) {
|
||||||
|
if (!entry) continue;
|
||||||
|
sourceIdMap.set(entry.sourceId, entry.prefixedId);
|
||||||
|
overlaySources[entry.prefixedId] = entry.normalizedSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayLayers: maplibregl.LayerSpecification[] = [];
|
||||||
|
for (const groupId of [
|
||||||
|
"rivers-line",
|
||||||
|
"bg-country-borders-line",
|
||||||
|
"bg-province-borders-line",
|
||||||
|
"bg-district-borders-line",
|
||||||
|
"country-labels",
|
||||||
|
] as const) {
|
||||||
|
const groupLayers = [...(selectedLayersByGroup.get(groupId) || [])].sort(compareOverlayLayers);
|
||||||
|
groupLayers.forEach((layer, index) => {
|
||||||
|
overlayLayers.push(
|
||||||
|
cloneOverlayLayer(layer, {
|
||||||
|
id: `goong-${groupId}-${index}`,
|
||||||
|
groupId,
|
||||||
|
sourceIdMap,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources: overlaySources,
|
||||||
|
layers: overlayLayers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGoongStyleDocument(styleUpstreamUrl: string): Promise<GoongStyleDocument> {
|
||||||
|
const existingPromise = goongStyleDocumentPromises.get(styleUpstreamUrl);
|
||||||
|
if (existingPromise) {
|
||||||
|
return existingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleProxyUrl = buildGoongProxyUrl(styleUpstreamUrl);
|
||||||
|
const promise = fetch(styleProxyUrl, { cache: "force-cache" })
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Goong style request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as GoongStyleDocument;
|
||||||
|
});
|
||||||
|
goongStyleDocumentPromises.set(styleUpstreamUrl, promise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (error) {
|
||||||
|
goongStyleDocumentPromises.delete(styleUpstreamUrl);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGoongSourceSpecification(
|
||||||
|
sourceUpstreamUrl: string,
|
||||||
|
parentSource: GoongStyleSource
|
||||||
|
): Promise<maplibregl.SourceSpecification> {
|
||||||
|
const cacheKey = JSON.stringify({
|
||||||
|
sourceUpstreamUrl,
|
||||||
|
type: parentSource.type,
|
||||||
|
tileSize: parentSource.tileSize,
|
||||||
|
minzoom: parentSource.minzoom,
|
||||||
|
maxzoom: parentSource.maxzoom,
|
||||||
|
});
|
||||||
|
const existingPromise = goongSourceSpecificationPromises.get(cacheKey);
|
||||||
|
if (existingPromise) {
|
||||||
|
return existingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceProxyUrl = buildGoongProxyUrl(sourceUpstreamUrl);
|
||||||
|
const promise = fetch(sourceProxyUrl, { cache: "force-cache" })
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Goong source request failed with status ${response.status}`);
|
||||||
|
}
|
||||||
|
return (await response.json()) as GoongSourceManifest;
|
||||||
|
})
|
||||||
|
.then((sourceDocument) =>
|
||||||
|
normalizeManifestBackedGoongSourceSpecification(parentSource, sourceDocument, sourceUpstreamUrl)
|
||||||
|
);
|
||||||
|
goongSourceSpecificationPromises.set(cacheKey, promise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (error) {
|
||||||
|
goongSourceSpecificationPromises.delete(cacheKey);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function normalizeGoongSourceSpecification(
|
||||||
|
source: GoongStyleSource,
|
||||||
|
parentDocumentUrl: string
|
||||||
|
): Promise<maplibregl.SourceSpecification> {
|
||||||
|
if (typeof source.url === "string" && source.url) {
|
||||||
|
const sourceUpstreamUrl = resolveGoongResourceUrl(source.url, parentDocumentUrl);
|
||||||
|
return loadGoongSourceSpecification(sourceUpstreamUrl, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeInlineGoongSourceSpecification(source, parentDocumentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeInlineGoongSourceSpecification(
|
||||||
|
source: GoongStyleSource,
|
||||||
|
parentDocumentUrl: string
|
||||||
|
): maplibregl.SourceSpecification {
|
||||||
|
return buildMapLibreSourceSpecification(source, parentDocumentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeManifestBackedGoongSourceSpecification(
|
||||||
|
parentSource: GoongStyleSource,
|
||||||
|
sourceManifest: GoongSourceManifest,
|
||||||
|
sourceUpstreamUrl: string
|
||||||
|
): maplibregl.SourceSpecification {
|
||||||
|
const mergedSource: GoongStyleSource = {
|
||||||
|
...parentSource,
|
||||||
|
attribution: sourceManifest.attribution ?? parentSource.attribution,
|
||||||
|
bounds: sourceManifest.bounds ?? parentSource.bounds,
|
||||||
|
maxzoom: sourceManifest.maxzoom ?? parentSource.maxzoom,
|
||||||
|
minzoom: sourceManifest.minzoom ?? parentSource.minzoom,
|
||||||
|
scheme: sourceManifest.scheme ?? parentSource.scheme,
|
||||||
|
tileSize:
|
||||||
|
sourceManifest.tileSize ??
|
||||||
|
normalizeGoongTileSize(sourceManifest.pixel_scale) ??
|
||||||
|
parentSource.tileSize,
|
||||||
|
tiles: sourceManifest.tiles ?? parentSource.tiles,
|
||||||
|
};
|
||||||
|
|
||||||
|
return buildMapLibreSourceSpecification(mergedSource, sourceUpstreamUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMapLibreSourceSpecification(
|
||||||
|
source: GoongStyleSource,
|
||||||
|
parentDocumentUrl: string
|
||||||
|
): maplibregl.SourceSpecification {
|
||||||
|
const resolvedTiles = Array.isArray(source.tiles)
|
||||||
|
? source.tiles.map((tileUrl) => {
|
||||||
|
const upstreamTileUrl = resolveGoongResourceUrl(tileUrl, parentDocumentUrl);
|
||||||
|
return buildGoongProxyUrl(upstreamTileUrl);
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (source.type === "raster") {
|
||||||
|
const rasterSource: maplibregl.RasterSourceSpecification = {
|
||||||
|
type: "raster",
|
||||||
|
...(resolvedTiles?.length ? { tiles: resolvedTiles } : {}),
|
||||||
|
...(typeof source.tileSize === "number" ? { tileSize: source.tileSize } : {}),
|
||||||
|
...(typeof source.minzoom === "number" ? { minzoom: source.minzoom } : {}),
|
||||||
|
...(typeof source.maxzoom === "number" ? { maxzoom: source.maxzoom } : {}),
|
||||||
|
...(Array.isArray(source.bounds) ? { bounds: source.bounds as [number, number, number, number] } : {}),
|
||||||
|
...(source.scheme ? { scheme: source.scheme } : {}),
|
||||||
|
...(source.attribution ? { attribution: source.attribution } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return rasterSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "vector") {
|
||||||
|
const vectorSource: maplibregl.VectorSourceSpecification = {
|
||||||
|
type: "vector",
|
||||||
|
...(resolvedTiles?.length ? { tiles: resolvedTiles } : {}),
|
||||||
|
...(typeof source.minzoom === "number" ? { minzoom: source.minzoom } : {}),
|
||||||
|
...(typeof source.maxzoom === "number" ? { maxzoom: source.maxzoom } : {}),
|
||||||
|
...(Array.isArray(source.bounds) ? { bounds: source.bounds as [number, number, number, number] } : {}),
|
||||||
|
...(source.scheme ? { scheme: source.scheme } : {}),
|
||||||
|
...(source.attribution ? { attribution: source.attribution } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return vectorSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported Goong source type: ${String(source.type || "unknown")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGoongTileSize(value: number | string | undefined): number | undefined {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const parsedValue = Number.parseInt(value, 10);
|
||||||
|
if (Number.isFinite(parsedValue)) {
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLayerReference(
|
||||||
|
layer: maplibregl.LayerSpecification,
|
||||||
|
layerById: Map<string, maplibregl.LayerSpecification>
|
||||||
|
): maplibregl.LayerSpecification {
|
||||||
|
const withRef = layer as maplibregl.LayerSpecification & { ref?: string };
|
||||||
|
if (!withRef.ref) {
|
||||||
|
return deepClone(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = layerById.get(withRef.ref);
|
||||||
|
if (!parent) {
|
||||||
|
return deepClone(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedParent = resolveLayerReference(parent, layerById);
|
||||||
|
const merged = {
|
||||||
|
...resolvedParent,
|
||||||
|
...deepClone(layer),
|
||||||
|
} as maplibregl.LayerSpecification & {
|
||||||
|
ref?: string;
|
||||||
|
layout?: Record<string, unknown>;
|
||||||
|
paint?: Record<string, unknown>;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
merged.layout = {
|
||||||
|
...(resolvedParent as { layout?: Record<string, unknown> }).layout,
|
||||||
|
...(withRef as { layout?: Record<string, unknown> }).layout,
|
||||||
|
};
|
||||||
|
merged.paint = {
|
||||||
|
...(resolvedParent as { paint?: Record<string, unknown> }).paint,
|
||||||
|
...(withRef as { paint?: Record<string, unknown> }).paint,
|
||||||
|
};
|
||||||
|
merged.metadata = {
|
||||||
|
...(resolvedParent as { metadata?: Record<string, unknown> }).metadata,
|
||||||
|
...(withRef as { metadata?: Record<string, unknown> }).metadata,
|
||||||
|
};
|
||||||
|
delete merged.ref;
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectGoongBackgroundGroup(
|
||||||
|
layer: maplibregl.LayerSpecification
|
||||||
|
): GoongBackgroundGroupId | null {
|
||||||
|
const haystack = [
|
||||||
|
layer.id,
|
||||||
|
"source" in layer && typeof layer.source === "string" ? layer.source : "",
|
||||||
|
"source-layer" in layer && typeof layer["source-layer"] === "string" ? layer["source-layer"] : "",
|
||||||
|
]
|
||||||
|
.join(" ")
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (layer.type === "symbol" && hasTextField(layer) && isPreferredPlaceLabelLayer(haystack)) {
|
||||||
|
return "country-labels";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.type === "line") {
|
||||||
|
const boundaryGroup = detectBoundaryGroup(layer, haystack);
|
||||||
|
if (boundaryGroup) {
|
||||||
|
return boundaryGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.type === "line" && /(water|waterway|river|stream|canal)/.test(haystack)) {
|
||||||
|
return "rivers-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.type === "fill" && /(water|lake|reservoir|sea|ocean)/.test(haystack)) {
|
||||||
|
return "rivers-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTextField(layer: maplibregl.LayerSpecification): boolean {
|
||||||
|
const layout = (layer as { layout?: Record<string, unknown> }).layout;
|
||||||
|
return Boolean(layout && "text-field" in layout && layout["text-field"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPreferredPlaceLabelLayer(haystack: string): boolean {
|
||||||
|
if (/(poi|airport|station|transit|rail|metro|bus|road|street|highway|path|route)/.test(haystack)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /(country|state|province|district|admin|place|city|town|village|settlement|capital|label)/.test(haystack);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectBoundaryGroup(
|
||||||
|
_layer: maplibregl.LayerSpecification,
|
||||||
|
haystack: string
|
||||||
|
): GoongBackgroundGroupId | null {
|
||||||
|
if (/(road|street|highway|path|route|rail|transit|water|waterway|river|stream|canal)/.test(haystack)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/(boundary|border|admin|country|state|province|district|ward|commune|county)/.test(haystack)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goong's public styles expose the boundary hierarchy most clearly
|
||||||
|
// through boundary-land-type-{0,1,2}. Prefer these exact matches over
|
||||||
|
// keyword heuristics because the heuristic buckets were mixing levels.
|
||||||
|
if (/boundary-land-type-0/.test(haystack)) {
|
||||||
|
if (/boundary-land-type-0-bg/.test(haystack)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "bg-country-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/boundary-land-type-1/.test(haystack)) {
|
||||||
|
if (/boundary-land-type-1-bg/.test(haystack)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "bg-province-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/boundary-land-type-2/.test(haystack)) {
|
||||||
|
return "bg-district-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminLevels = extractAdminLevels(haystack);
|
||||||
|
if (adminLevels.length > 0) {
|
||||||
|
const minAdminLevel = Math.min(...adminLevels);
|
||||||
|
if (minAdminLevel <= 2) return "bg-country-borders-line";
|
||||||
|
if (minAdminLevel <= 5) return "bg-province-borders-line";
|
||||||
|
return "bg-district-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(district|ward|commune|subdistrict|neighbou?rhood)/.test(haystack)) {
|
||||||
|
return "bg-district-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(province|state|region)/.test(haystack)) {
|
||||||
|
return "bg-province-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(country|national|international)/.test(haystack)) {
|
||||||
|
return "bg-country-borders-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAdminLevels(haystack: string): number[] {
|
||||||
|
const matches = Array.from(
|
||||||
|
haystack.matchAll(/(?:admin[_ -]?level|adminlevel|admin|level)[_ -]?(\d{1,2})/g)
|
||||||
|
);
|
||||||
|
|
||||||
|
return matches
|
||||||
|
.map((match) => Number.parseInt(match[1] || "", 10))
|
||||||
|
.filter((value) => Number.isFinite(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneOverlayLayer(
|
||||||
|
layer: maplibregl.LayerSpecification,
|
||||||
|
options: {
|
||||||
|
id: string;
|
||||||
|
groupId: GoongBackgroundGroupId;
|
||||||
|
sourceIdMap: Map<string, string>;
|
||||||
|
}
|
||||||
|
): maplibregl.LayerSpecification {
|
||||||
|
const cloned = deepClone(layer) as maplibregl.LayerSpecification & {
|
||||||
|
source?: string;
|
||||||
|
layout?: Record<string, unknown>;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
cloned.id = options.id;
|
||||||
|
if (typeof cloned.source === "string" && options.sourceIdMap.has(cloned.source)) {
|
||||||
|
cloned.source = options.sourceIdMap.get(cloned.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned.metadata = {
|
||||||
|
...(cloned.metadata || {}),
|
||||||
|
uhmBackgroundGroupId: options.groupId,
|
||||||
|
uhmBackgroundProvider: "goong",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.groupId === "country-labels") {
|
||||||
|
const layout = { ...(cloned.layout || {}) };
|
||||||
|
delete layout["icon-image"];
|
||||||
|
delete layout["icon-size"];
|
||||||
|
delete layout["icon-allow-overlap"];
|
||||||
|
delete layout["icon-ignore-placement"];
|
||||||
|
if (!Array.isArray(layout["text-font"])) {
|
||||||
|
layout["text-font"] = [...GOONG_LABEL_FALLBACK_FONT_STACK];
|
||||||
|
}
|
||||||
|
cloned.layout = layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareOverlayLayers(
|
||||||
|
left: maplibregl.LayerSpecification,
|
||||||
|
right: maplibregl.LayerSpecification
|
||||||
|
): number {
|
||||||
|
const leftMinzoom = "minzoom" in left && typeof left.minzoom === "number"
|
||||||
|
? left.minzoom
|
||||||
|
: -1;
|
||||||
|
const rightMinzoom = "minzoom" in right && typeof right.minzoom === "number"
|
||||||
|
? right.minzoom
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
if (leftMinzoom !== rightMinzoom) {
|
||||||
|
return leftMinzoom - rightMinzoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.id.localeCompare(right.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveGoongResourceUrl(value: string, parentDocumentUrl: string): string {
|
||||||
|
if (/^[a-z]+:\/\//i.test(value) || value.startsWith("data:")) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(value, parentDocumentUrl).toString();
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepClone<T>(value: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(value)) as T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/use
|
|||||||
import {
|
import {
|
||||||
FEATURE_STATE_SOURCE_IDS,
|
FEATURE_STATE_SOURCE_IDS,
|
||||||
PATH_ARROW_ICON_ID,
|
PATH_ARROW_ICON_ID,
|
||||||
RASTER_BASE_INSERT_BEFORE_LAYER_ID,
|
|
||||||
RASTER_BASE_LAYER_ID,
|
RASTER_BASE_LAYER_ID,
|
||||||
RASTER_BASE_SOURCE_ID,
|
RASTER_BASE_SOURCE_ID,
|
||||||
PATH_ARROW_SOURCE_ID
|
PATH_ARROW_SOURCE_ID
|
||||||
} from "@/uhm/lib/map/constants";
|
} from "@/uhm/lib/map/constants";
|
||||||
import { PATH_RENDER_BY_TYPE } from "@/uhm/lib/map/styles/style";
|
import { PATH_RENDER_BY_TYPE } from "@/uhm/lib/map/styles/style";
|
||||||
import { getRasterTileTemplateUrl } from "@/uhm/api/tiles";
|
import { getBackgroundRasterSourceSpecification } from "@/uhm/api/tiles";
|
||||||
import { newId } from "@/uhm/lib/utils/id";
|
import { newId } from "@/uhm/lib/utils/id";
|
||||||
import { normalizeGeoTypeKey } from "@/uhm/lib/map/geo/geoTypeMap";
|
import { normalizeGeoTypeKey } from "@/uhm/lib/map/geo/geoTypeMap";
|
||||||
|
|
||||||
@@ -30,33 +29,46 @@ export function applyBackgroundLayerVisibility(
|
|||||||
|
|
||||||
for (const layer of BACKGROUND_LAYER_OPTIONS) {
|
for (const layer of BACKGROUND_LAYER_OPTIONS) {
|
||||||
if (layer.id === RASTER_BASE_LAYER_ID) continue;
|
if (layer.id === RASTER_BASE_LAYER_ID) continue;
|
||||||
if (!map.getLayer(layer.id)) continue;
|
const nextVisibility = visibility[layer.id] ? "visible" : "none";
|
||||||
map.setLayoutProperty(
|
|
||||||
layer.id,
|
if (map.getLayer(layer.id)) {
|
||||||
"visibility",
|
map.setLayoutProperty(layer.id, "visibility", nextVisibility);
|
||||||
visibility[layer.id] ? "visible" : "none"
|
}
|
||||||
);
|
|
||||||
|
const groupedLayerIds = getBackgroundGroupLayerIds(map, layer.id);
|
||||||
|
for (const groupedLayerId of groupedLayerIds) {
|
||||||
|
if (!map.getLayer(groupedLayerId)) continue;
|
||||||
|
map.setLayoutProperty(groupedLayerId, "visibility", nextVisibility);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncRasterBaseVisibility(map: maplibregl.Map, shouldShow: boolean) {
|
export function syncRasterBaseVisibility(map: maplibregl.Map, shouldShow: boolean) {
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
ensureRasterBaseLayer(map);
|
void ensureRasterBaseLayer(map).catch((error) => {
|
||||||
|
console.error("Failed to load proxied raster background.", error);
|
||||||
|
removeRasterBaseLayer(map);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeRasterBaseLayer(map);
|
removeRasterBaseLayer(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureRasterBaseLayer(map: maplibregl.Map) {
|
export async function ensureRasterBaseLayer(map: maplibregl.Map) {
|
||||||
if (!map.getSource(RASTER_BASE_SOURCE_ID)) {
|
if (!map.getSource(RASTER_BASE_SOURCE_ID)) {
|
||||||
map.addSource(RASTER_BASE_SOURCE_ID, createRasterBaseSource());
|
const source = await createRasterBaseSource();
|
||||||
|
if (map.getSource(RASTER_BASE_SOURCE_ID)) {
|
||||||
|
// Another caller already added the source while we were waiting.
|
||||||
|
} else {
|
||||||
|
map.addSource(RASTER_BASE_SOURCE_ID, source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const beforeId = getRasterBaseInsertBeforeLayerId(map);
|
||||||
if (!map.getLayer(RASTER_BASE_LAYER_ID)) {
|
if (!map.getLayer(RASTER_BASE_LAYER_ID)) {
|
||||||
const beforeId = map.getLayer(RASTER_BASE_INSERT_BEFORE_LAYER_ID)
|
|
||||||
? RASTER_BASE_INSERT_BEFORE_LAYER_ID
|
|
||||||
: undefined;
|
|
||||||
map.addLayer(createRasterBaseLayer(), beforeId);
|
map.addLayer(createRasterBaseLayer(), beforeId);
|
||||||
|
} else if (beforeId && beforeId !== RASTER_BASE_LAYER_ID) {
|
||||||
|
map.moveLayer(RASTER_BASE_LAYER_ID, beforeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
map.setLayoutProperty(RASTER_BASE_LAYER_ID, "visibility", "visible");
|
map.setLayoutProperty(RASTER_BASE_LAYER_ID, "visibility", "visible");
|
||||||
@@ -73,13 +85,7 @@ export function removeRasterBaseLayer(map: maplibregl.Map) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createRasterBaseSource() {
|
export function createRasterBaseSource() {
|
||||||
return {
|
return getBackgroundRasterSourceSpecification();
|
||||||
type: "raster" as const,
|
|
||||||
tiles: [getRasterTileTemplateUrl()],
|
|
||||||
tileSize: 256,
|
|
||||||
minzoom: 0,
|
|
||||||
maxzoom: 6,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRasterBaseLayer() {
|
export function createRasterBaseLayer() {
|
||||||
@@ -94,6 +100,30 @@ export function createRasterBaseLayer() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRasterBaseInsertBeforeLayerId(map: maplibregl.Map): string | undefined {
|
||||||
|
const style = map.getStyle();
|
||||||
|
const layers = style?.layers || [];
|
||||||
|
|
||||||
|
return layers.find((layer) => {
|
||||||
|
return layer.id !== "background" && layer.id !== RASTER_BASE_LAYER_ID;
|
||||||
|
})?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBackgroundGroupLayerIds(
|
||||||
|
map: maplibregl.Map,
|
||||||
|
groupId: string
|
||||||
|
): string[] {
|
||||||
|
const style = map.getStyle();
|
||||||
|
if (!style?.layers?.length) return [];
|
||||||
|
|
||||||
|
return style.layers
|
||||||
|
.filter((layer) => {
|
||||||
|
const metadata = (layer as { metadata?: Record<string, unknown> }).metadata;
|
||||||
|
return metadata?.uhmBackgroundGroupId === groupId;
|
||||||
|
})
|
||||||
|
.map((layer) => layer.id);
|
||||||
|
}
|
||||||
|
|
||||||
export function getSelectableLayers(map: maplibregl.Map): string[] {
|
export function getSelectableLayers(map: maplibregl.Map): string[] {
|
||||||
const selectableSources = ["countries", "places", PATH_ARROW_SOURCE_ID];
|
const selectableSources = ["countries", "places", PATH_ARROW_SOURCE_ID];
|
||||||
const style = map.getStyle();
|
const style = map.getStyle();
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import { getVectorTileTemplateUrl } from "@/uhm/api/tiles";
|
import { GOONG_GLYPHS_PROXY_URL } from "@/uhm/api/config";
|
||||||
import {
|
import { getGoongBackgroundOverlayBundle } from "@/uhm/api/tiles";
|
||||||
COUNTRY_FILL_COLOR_EXPRESSION,
|
|
||||||
LINE_COLOR_BY_TYPE,
|
|
||||||
PATH_RENDER_BY_TYPE,
|
|
||||||
POLYGON_FILL_BY_TYPE,
|
|
||||||
POLYGON_OPACITY_BY_TYPE,
|
|
||||||
POLYGON_STROKE_BY_TYPE,
|
|
||||||
} from "@/uhm/lib/map/styles/style";
|
|
||||||
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
||||||
import { PATH_ARROW_ICON_ID, PATH_ARROW_SOURCE_ID, POLYGON_LABEL_SOURCE_ID } from "@/uhm/lib/map/constants";
|
import { PATH_ARROW_ICON_ID, PATH_ARROW_SOURCE_ID, POLYGON_LABEL_SOURCE_ID } from "@/uhm/lib/map/constants";
|
||||||
import { ensurePointGeotypeIcons, getAllGeotypeLabelLayers, getAllGeotypeLayers } from "@/uhm/lib/map/styles/geotypeLayers";
|
import { ensurePointGeotypeIcons, getAllGeotypeLabelLayers, getAllGeotypeLayers } from "@/uhm/lib/map/styles/geotypeLayers";
|
||||||
import {
|
import {
|
||||||
applyBackgroundLayerVisibility,
|
applyBackgroundLayerVisibility,
|
||||||
buildTypeMatchExpression,
|
|
||||||
ensurePathArrowIcon,
|
ensurePathArrowIcon,
|
||||||
} from "./mapUtils";
|
} from "./mapUtils";
|
||||||
import { BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers";
|
import { BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers";
|
||||||
@@ -23,15 +14,8 @@ import { FeatureCollection } from "@/uhm/lib/editor/state/useEditorState";
|
|||||||
export function getBaseMapStyle(): maplibregl.StyleSpecification {
|
export function getBaseMapStyle(): maplibregl.StyleSpecification {
|
||||||
return {
|
return {
|
||||||
version: 8,
|
version: 8,
|
||||||
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
|
glyphs: GOONG_GLYPHS_PROXY_URL,
|
||||||
sources: {
|
sources: {},
|
||||||
base: {
|
|
||||||
type: "vector",
|
|
||||||
tiles: [getVectorTileTemplateUrl()],
|
|
||||||
minzoom: 0,
|
|
||||||
maxzoom: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
id: "background",
|
id: "background",
|
||||||
@@ -40,157 +24,6 @@ export function getBaseMapStyle(): maplibregl.StyleSpecification {
|
|||||||
"background-color": "#0b1220",
|
"background-color": "#0b1220",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "graticules-line",
|
|
||||||
type: "line",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_graticules_10",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#334155",
|
|
||||||
"line-width": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
0, 0.3,
|
|
||||||
4, 0.6,
|
|
||||||
6, 0.8,
|
|
||||||
],
|
|
||||||
"line-opacity": 0.55,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "land",
|
|
||||||
type: "fill",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_land",
|
|
||||||
paint: {
|
|
||||||
"fill-color": "#1e293b",
|
|
||||||
"fill-opacity": 0.25,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bg-countries-fill",
|
|
||||||
type: "fill",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_admin_0_countries",
|
|
||||||
paint: {
|
|
||||||
"fill-color": COUNTRY_FILL_COLOR_EXPRESSION,
|
|
||||||
"fill-opacity": 0.38,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bg-country-borders-line",
|
|
||||||
type: "line",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_admin_0_boundary_lines_land",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#cbd5e1",
|
|
||||||
"line-width": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
0, 0.2,
|
|
||||||
4, 0.5,
|
|
||||||
6, 1.1,
|
|
||||||
],
|
|
||||||
"line-opacity": 0.85,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "country-labels",
|
|
||||||
type: "symbol",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "country_labels",
|
|
||||||
minzoom: 0,
|
|
||||||
layout: {
|
|
||||||
"text-field": [
|
|
||||||
"coalesce",
|
|
||||||
["get", "NAME_EN"],
|
|
||||||
["get", "NAME"],
|
|
||||||
["get", "ADMIN"],
|
|
||||||
["get", "name"],
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
"text-size": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
0, 15,
|
|
||||||
1, 16,
|
|
||||||
2, 17,
|
|
||||||
4, 19,
|
|
||||||
6, 23,
|
|
||||||
],
|
|
||||||
"text-padding": 0,
|
|
||||||
"text-max-width": 10,
|
|
||||||
"text-allow-overlap": true,
|
|
||||||
"text-ignore-placement": true,
|
|
||||||
"symbol-placement": "point",
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"text-color": "#e2e8f0",
|
|
||||||
"text-halo-color": "#0b1220",
|
|
||||||
"text-halo-width": 1.2,
|
|
||||||
"text-halo-blur": 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "regions-line",
|
|
||||||
type: "line",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_geography_regions_polys",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#475569",
|
|
||||||
"line-width": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
0, 0.2,
|
|
||||||
4, 0.6,
|
|
||||||
6, 1,
|
|
||||||
],
|
|
||||||
"line-opacity": 0.6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "lakes-fill",
|
|
||||||
type: "fill",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_lakes",
|
|
||||||
paint: {
|
|
||||||
"fill-color": "#1d4ed8",
|
|
||||||
"fill-opacity": 0.45,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "rivers-line",
|
|
||||||
type: "line",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_rivers_lake_centerlines",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#38bdf8",
|
|
||||||
"line-width": [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["zoom"],
|
|
||||||
0, 0.25,
|
|
||||||
4, 0.8,
|
|
||||||
6, 1.5,
|
|
||||||
],
|
|
||||||
"line-opacity": 0.85,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "geolines-line",
|
|
||||||
type: "line",
|
|
||||||
source: "base",
|
|
||||||
"source-layer": "ne_10m_geographic_lines",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#94a3b8",
|
|
||||||
"line-width": 1.2,
|
|
||||||
"line-opacity": 0.8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -202,6 +35,9 @@ export function setupMapLayers(
|
|||||||
applyHighlightToMap: (fc: FeatureCollection) => void
|
applyHighlightToMap: (fc: FeatureCollection) => void
|
||||||
) {
|
) {
|
||||||
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
||||||
|
void replaceBackgroundLayersWithGoong(map, backgroundVisibility).catch((error) => {
|
||||||
|
console.error("Failed to load proxied background overlay bundle.", error);
|
||||||
|
});
|
||||||
const hasPathArrowIcon = ensurePathArrowIcon(map);
|
const hasPathArrowIcon = ensurePathArrowIcon(map);
|
||||||
|
|
||||||
// preview (drawing)
|
// preview (drawing)
|
||||||
@@ -432,3 +268,29 @@ export function setupMapLayers(
|
|||||||
});
|
});
|
||||||
applyHighlightToMap(highlightFeatures || EMPTY_FEATURE_COLLECTION);
|
applyHighlightToMap(highlightFeatures || EMPTY_FEATURE_COLLECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function replaceBackgroundLayersWithGoong(
|
||||||
|
map: maplibregl.Map,
|
||||||
|
backgroundVisibility: BackgroundLayerVisibility
|
||||||
|
) {
|
||||||
|
const bundle = await getGoongBackgroundOverlayBundle();
|
||||||
|
if (!bundle || map.getLayer("goong-country-labels-0")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [sourceId, source] of Object.entries(bundle.sources)) {
|
||||||
|
if (!map.getSource(sourceId)) {
|
||||||
|
map.addSource(sourceId, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertBeforeId = map.getLayer("draw-preview-fill")
|
||||||
|
? "draw-preview-fill"
|
||||||
|
: undefined;
|
||||||
|
for (const layer of bundle.layers) {
|
||||||
|
if (map.getLayer(layer.id)) continue;
|
||||||
|
map.addLayer(layer, insertBeforeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
# Goong APIs In Use
|
||||||
|
|
||||||
|
Mục tiêu của tài liệu này:
|
||||||
|
|
||||||
|
- mô tả **chính xác** frontend hiện tại đang dùng gì từ Goong
|
||||||
|
- mô tả **backend cần proxy gì** để giấu `api_key`
|
||||||
|
- mô tả **response nào phải rewrite**
|
||||||
|
- tránh liệt kê thừa các API Goong mà app hiện tại không đụng tới
|
||||||
|
|
||||||
|
Phạm vi kiểm tra:
|
||||||
|
|
||||||
|
- [config.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/config.ts:1)
|
||||||
|
- [tiles.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/tiles.ts:1)
|
||||||
|
- [useMapLayers.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/components/map/useMapLayers.ts:1)
|
||||||
|
- style JSON đã tải về:
|
||||||
|
- [goong_map_web.json](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/tmp/goong-styles/goong_map_web.json)
|
||||||
|
- [goong_satellite.json](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/tmp/goong-styles/goong_satellite.json)
|
||||||
|
|
||||||
|
## 1. Tóm tắt kỹ thuật
|
||||||
|
|
||||||
|
Frontend hiện tại **không** `map.setStyle(goongStyleJson)` trực tiếp.
|
||||||
|
|
||||||
|
Thay vào đó:
|
||||||
|
|
||||||
|
1. app tự `fetch()` 2 style JSON của Goong
|
||||||
|
2. app parse style JSON để lấy:
|
||||||
|
- `raster source` từ `goong_satellite.json`
|
||||||
|
- `sources + layers` cần thiết từ `goong_map_web.json`
|
||||||
|
3. app `map.addSource(...)` và `map.addLayer(...)` thủ công
|
||||||
|
4. từ thời điểm đó, **MapLibre tự request tiếp** các `source.url`
|
||||||
|
5. rồi từ các source manifest đó, **MapLibre lại tự request tiếp** các tile URLs nằm trong `tiles[]`
|
||||||
|
|
||||||
|
Hệ quả:
|
||||||
|
|
||||||
|
- nếu BE chỉ proxy `assets/*.json` thì **chưa đủ**
|
||||||
|
- nếu BE chỉ proxy `sources/*.json` mà **không rewrite `tiles[]`** thì **vẫn lộ key ở request tile**
|
||||||
|
|
||||||
|
## 2. Luồng request thật hiện tại
|
||||||
|
|
||||||
|
### 2.1. App fetch trực tiếp style JSON
|
||||||
|
|
||||||
|
Frontend gọi trực tiếp:
|
||||||
|
|
||||||
|
1. `https://tiles.goong.io/assets/goong_satellite.json?api_key=...`
|
||||||
|
2. `https://tiles.goong.io/assets/goong_map_web.json?api_key=...`
|
||||||
|
|
||||||
|
Nguồn trong code:
|
||||||
|
|
||||||
|
- `GOONG_SATELLITE_STYLE_URL` ở [config.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/config.ts:15)
|
||||||
|
- `GOONG_VECTOR_OVERLAY_STYLE_URL` ở [config.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/config.ts:19)
|
||||||
|
- `loadGoongStyleDocument(...)` ở [tiles.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/tiles.ts:211)
|
||||||
|
|
||||||
|
Mục đích:
|
||||||
|
|
||||||
|
- `goong_satellite.json`
|
||||||
|
- app lấy ra raster source đầu tiên
|
||||||
|
- dùng làm nền satellite
|
||||||
|
- `goong_map_web.json`
|
||||||
|
- app lấy ra các layer/source phục vụ:
|
||||||
|
- `Country Borders`
|
||||||
|
- `Province Borders`
|
||||||
|
- `District Borders`
|
||||||
|
- `Country Labels`
|
||||||
|
- `Rivers`
|
||||||
|
|
||||||
|
### 2.2. MapLibre fetch source manifests
|
||||||
|
|
||||||
|
Sau khi app clone source spec từ style JSON và `addSource(...)`, MapLibre tự bắn tiếp các request `source.url`.
|
||||||
|
|
||||||
|
Các source URL đang xuất hiện trong style JSON:
|
||||||
|
|
||||||
|
#### Trong `goong_satellite.json`
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/sources/satellite.json?api_key=...`
|
||||||
|
- `https://tiles.goong.io/sources/base.json?api_key=...`
|
||||||
|
- `https://tiles.goong.io/sources/goong.json?api_key=...`
|
||||||
|
|
||||||
|
#### Trong `goong_map_web.json`
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/sources/base.json?api_key=...`
|
||||||
|
- `https://tiles.goong.io/sources/goong.json?api_key=...`
|
||||||
|
|
||||||
|
Ý nghĩa:
|
||||||
|
|
||||||
|
- `sources/satellite.json`
|
||||||
|
- raster source manifest cho nền satellite
|
||||||
|
- `sources/base.json`
|
||||||
|
- vector source manifest cho các lớp `boundary`, `worldcountriespoints`, `worldnationalcapitals`
|
||||||
|
- `sources/goong.json`
|
||||||
|
- vector source manifest cho các lớp `riversandlakes`, `vietnam_administrator`
|
||||||
|
|
||||||
|
### 2.3. MapLibre fetch tile URLs nằm trong source manifests
|
||||||
|
|
||||||
|
Đây là phần dễ bị bỏ sót nhất.
|
||||||
|
|
||||||
|
Khi MapLibre đã tải `sources/satellite.json`, `sources/base.json`, `sources/goong.json`, nó sẽ tiếp tục request các URL nằm trong field:
|
||||||
|
|
||||||
|
- `tiles[]`
|
||||||
|
|
||||||
|
Tức là runtime thật của frontend hiện tại là:
|
||||||
|
|
||||||
|
1. fetch style JSON
|
||||||
|
2. fetch source manifest
|
||||||
|
3. fetch tile URL bên trong source manifest
|
||||||
|
|
||||||
|
Nếu backend muốn che key hoàn toàn, thì **bước 3 bắt buộc phải được proxy hoặc rewrite về domain backend**.
|
||||||
|
|
||||||
|
## 3. Những upstream Goong resource đang dùng thật
|
||||||
|
|
||||||
|
Tính theo runtime hiện tại, upstream Goong đang được dùng thật là:
|
||||||
|
|
||||||
|
### 3.1. Style JSON
|
||||||
|
|
||||||
|
- `assets/goong_satellite.json`
|
||||||
|
- `assets/goong_map_web.json`
|
||||||
|
|
||||||
|
### 3.2. Source manifests
|
||||||
|
|
||||||
|
- `sources/satellite.json`
|
||||||
|
- `sources/base.json`
|
||||||
|
- `sources/goong.json`
|
||||||
|
|
||||||
|
### 3.3. Tile endpoints bên trong source manifests
|
||||||
|
|
||||||
|
- raster tile URLs nằm trong `sources/satellite.json`
|
||||||
|
- vector tile URLs nằm trong `sources/base.json`
|
||||||
|
- vector tile URLs nằm trong `sources/goong.json`
|
||||||
|
|
||||||
|
Lưu ý:
|
||||||
|
|
||||||
|
- tile URL pattern chính xác phải đọc từ source manifest upstream ở runtime
|
||||||
|
- backend không nên hardcode khi chưa xác minh nội dung `tiles[]`
|
||||||
|
|
||||||
|
## 4. Những thứ frontend hiện tại dùng thêm hoặc KHÔNG dùng
|
||||||
|
|
||||||
|
### 4.1. Goong glyphs / fonts
|
||||||
|
|
||||||
|
Style JSON của Goong có field:
|
||||||
|
|
||||||
|
- `glyphs: https://tiles.goong.io/fonts/{fontstack}/{range}.pbf?api_key=...`
|
||||||
|
|
||||||
|
Flow hiện tại **có dùng glyphs của Goong qua proxy**.
|
||||||
|
|
||||||
|
Map đang trỏ `glyphs` vào:
|
||||||
|
|
||||||
|
- `/proxy/{encoded-https://tiles.goong.io/fonts/{fontstack}/{range}.pbf}`
|
||||||
|
|
||||||
|
Nguồn trong code:
|
||||||
|
|
||||||
|
- [useMapLayers.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/components/map/useMapLayers.ts:17)
|
||||||
|
- [config.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/config.ts:12)
|
||||||
|
|
||||||
|
Kết luận:
|
||||||
|
|
||||||
|
- **backend proxy Goong fonts/glyphs là bắt buộc cho flow hiện tại**
|
||||||
|
|
||||||
|
### 4.2. Goong sprite
|
||||||
|
|
||||||
|
Style JSON của Goong có:
|
||||||
|
|
||||||
|
- `sprite: https://tiles.goong.io/sprite`
|
||||||
|
|
||||||
|
Nhưng flow hiện tại **không phụ thuộc sprite** vì:
|
||||||
|
|
||||||
|
- app không nạp toàn bộ Goong style vào map
|
||||||
|
- app chỉ nhặt `sources` và `layers`
|
||||||
|
- khi clone overlay labels, code còn chủ động loại bớt icon fields
|
||||||
|
|
||||||
|
Nguồn trong code:
|
||||||
|
|
||||||
|
- `cloneOverlayLayer(...)` ở [tiles.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/tiles.ts:411)
|
||||||
|
|
||||||
|
Kết luận:
|
||||||
|
|
||||||
|
- **không cần backend proxy Goong sprite cho flow hiện tại**
|
||||||
|
|
||||||
|
### 4.3. Các REST API khác của Goong
|
||||||
|
|
||||||
|
Không dùng:
|
||||||
|
|
||||||
|
- geocoding
|
||||||
|
- autocomplete
|
||||||
|
- directions
|
||||||
|
- distance matrix
|
||||||
|
- place details
|
||||||
|
- static map
|
||||||
|
|
||||||
|
## 5. Backend cần làm gì
|
||||||
|
|
||||||
|
### 5.1. Mục tiêu backend
|
||||||
|
|
||||||
|
Backend phải đảm bảo:
|
||||||
|
|
||||||
|
1. browser không gọi Goong trực tiếp
|
||||||
|
2. browser không nhìn thấy `api_key`
|
||||||
|
3. frontend vẫn nhận được dữ liệu theo format mà MapLibre/app hiện tại cần
|
||||||
|
|
||||||
|
### 5.2. Hai kiểu triển khai
|
||||||
|
|
||||||
|
Có 2 cách:
|
||||||
|
|
||||||
|
#### Cách A: Transparent proxy
|
||||||
|
|
||||||
|
BE trả về gần như đúng response của Goong, chỉ rewrite URL.
|
||||||
|
|
||||||
|
Ưu điểm:
|
||||||
|
|
||||||
|
- gần với Goong
|
||||||
|
- ít phải đổi frontend hơn
|
||||||
|
|
||||||
|
Nhược điểm:
|
||||||
|
|
||||||
|
- BE phải rewrite nhiều chỗ
|
||||||
|
|
||||||
|
#### Cách B: Normalize thành API nội bộ
|
||||||
|
|
||||||
|
BE không trả nguyên style/source của Goong mà trả dữ liệu đã xử lý sẵn cho FE.
|
||||||
|
|
||||||
|
Ưu điểm:
|
||||||
|
|
||||||
|
- hợp đồng BE-FE rõ hơn
|
||||||
|
- ít phụ thuộc format Goong hơn
|
||||||
|
|
||||||
|
Nhược điểm:
|
||||||
|
|
||||||
|
- cần sửa frontend nhiều hơn
|
||||||
|
|
||||||
|
Với frontend hiện tại, **Cách A** là hợp lý nhất.
|
||||||
|
|
||||||
|
## 6. Contract backend được khuyến nghị
|
||||||
|
|
||||||
|
### 6.1. Proxy style JSON
|
||||||
|
|
||||||
|
#### `GET /proxy/goong/assets/goong_satellite.json`
|
||||||
|
|
||||||
|
Upstream:
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/assets/goong_satellite.json?api_key=<server-side-key>`
|
||||||
|
|
||||||
|
Backend phải:
|
||||||
|
|
||||||
|
- fetch upstream bằng key server-side
|
||||||
|
- parse JSON
|
||||||
|
- rewrite `sources.*.url` về domain backend
|
||||||
|
- có thể giữ nguyên các field khác
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- body: style JSON đã rewrite
|
||||||
|
|
||||||
|
#### `GET /proxy/goong/assets/goong_map_web.json`
|
||||||
|
|
||||||
|
Upstream:
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/assets/goong_map_web.json?api_key=<server-side-key>`
|
||||||
|
|
||||||
|
Backend phải:
|
||||||
|
|
||||||
|
- fetch upstream bằng key server-side
|
||||||
|
- parse JSON
|
||||||
|
- rewrite `sources.*.url` về domain backend
|
||||||
|
- có thể giữ nguyên các field khác
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- body: style JSON đã rewrite
|
||||||
|
|
||||||
|
### 6.2. Proxy source manifests
|
||||||
|
|
||||||
|
#### `GET /proxy/goong/sources/satellite.json`
|
||||||
|
|
||||||
|
Upstream:
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/sources/satellite.json?api_key=<server-side-key>`
|
||||||
|
|
||||||
|
Backend phải:
|
||||||
|
|
||||||
|
- fetch upstream
|
||||||
|
- parse JSON
|
||||||
|
- rewrite mọi URL trong `tiles[]` về domain backend
|
||||||
|
- giữ nguyên metadata quan trọng:
|
||||||
|
- `tileSize`
|
||||||
|
- `minzoom`
|
||||||
|
- `maxzoom`
|
||||||
|
- `bounds`
|
||||||
|
- `scheme`
|
||||||
|
- `attribution`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- body: source manifest đã rewrite
|
||||||
|
|
||||||
|
#### `GET /proxy/goong/sources/base.json`
|
||||||
|
|
||||||
|
Upstream:
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/sources/base.json?api_key=<server-side-key>`
|
||||||
|
|
||||||
|
Backend phải:
|
||||||
|
|
||||||
|
- fetch upstream
|
||||||
|
- parse JSON
|
||||||
|
- rewrite mọi URL trong `tiles[]` về domain backend
|
||||||
|
- giữ nguyên metadata tilejson khác
|
||||||
|
|
||||||
|
#### `GET /proxy/goong/sources/goong.json`
|
||||||
|
|
||||||
|
Upstream:
|
||||||
|
|
||||||
|
- `https://tiles.goong.io/sources/goong.json?api_key=<server-side-key>`
|
||||||
|
|
||||||
|
Backend phải:
|
||||||
|
|
||||||
|
- fetch upstream
|
||||||
|
- parse JSON
|
||||||
|
- rewrite mọi URL trong `tiles[]` về domain backend
|
||||||
|
- giữ nguyên metadata tilejson khác
|
||||||
|
|
||||||
|
### 6.3. Proxy tile endpoints
|
||||||
|
|
||||||
|
Backend bắt buộc phải có route để trả tile thật.
|
||||||
|
|
||||||
|
Có thể làm generic, ví dụ:
|
||||||
|
|
||||||
|
- `GET /proxy/goong/tiles/*`
|
||||||
|
|
||||||
|
hoặc explicit hơn theo source:
|
||||||
|
|
||||||
|
- `GET /proxy/goong/tiles/satellite/...`
|
||||||
|
- `GET /proxy/goong/tiles/base/...`
|
||||||
|
- `GET /proxy/goong/tiles/goong/...`
|
||||||
|
|
||||||
|
Yêu cầu:
|
||||||
|
|
||||||
|
- request browser -> backend
|
||||||
|
- backend -> upstream Goong bằng key server-side
|
||||||
|
- stream response về browser
|
||||||
|
- pass through hoặc preserve:
|
||||||
|
- `Content-Type`
|
||||||
|
- `Cache-Control`
|
||||||
|
- `ETag`
|
||||||
|
- `Last-Modified`
|
||||||
|
|
||||||
|
Response type có thể là:
|
||||||
|
|
||||||
|
- raster image
|
||||||
|
- vector tile protobuf
|
||||||
|
|
||||||
|
## 7. Runtime dependency map cho BE
|
||||||
|
|
||||||
|
### 7.1. Satellite background
|
||||||
|
|
||||||
|
Luồng:
|
||||||
|
|
||||||
|
1. FE đọc `goong_satellite.json`
|
||||||
|
2. FE lấy `sources.satellite`
|
||||||
|
3. MapLibre gọi `sources/satellite.json`
|
||||||
|
4. MapLibre gọi raster tile URLs trong `tiles[]`
|
||||||
|
|
||||||
|
BE cần cover:
|
||||||
|
|
||||||
|
- style JSON
|
||||||
|
- source manifest
|
||||||
|
- raster tile URLs
|
||||||
|
|
||||||
|
### 7.2. Overlay borders / labels / rivers
|
||||||
|
|
||||||
|
Luồng:
|
||||||
|
|
||||||
|
1. FE đọc `goong_map_web.json`
|
||||||
|
2. FE lấy selected layers + selected sources
|
||||||
|
3. MapLibre gọi `sources/base.json`
|
||||||
|
4. MapLibre gọi `sources/goong.json`
|
||||||
|
5. MapLibre gọi vector tile URLs của 2 source manifest này
|
||||||
|
|
||||||
|
BE cần cover:
|
||||||
|
|
||||||
|
- style JSON
|
||||||
|
- 2 source manifests
|
||||||
|
- vector tile URLs tương ứng
|
||||||
|
|
||||||
|
## 8. Danh sách tối thiểu BE phải cover
|
||||||
|
|
||||||
|
Nếu chỉ làm đúng những gì frontend hiện tại dùng, checklist tối thiểu là:
|
||||||
|
|
||||||
|
1. proxy `assets/goong_satellite.json`
|
||||||
|
2. proxy `assets/goong_map_web.json`
|
||||||
|
3. proxy `sources/satellite.json`
|
||||||
|
4. proxy `sources/base.json`
|
||||||
|
5. proxy `sources/goong.json`
|
||||||
|
6. proxy toàn bộ tile URL được khai báo trong `sources/satellite.json`
|
||||||
|
7. proxy toàn bộ tile URL được khai báo trong `sources/base.json`
|
||||||
|
8. proxy toàn bộ tile URL được khai báo trong `sources/goong.json`
|
||||||
|
|
||||||
|
## 9. Những gì BE chưa cần làm ngay
|
||||||
|
|
||||||
|
Cho flow hiện tại, BE **chưa cần**:
|
||||||
|
|
||||||
|
- proxy Goong `glyphs`
|
||||||
|
- proxy Goong `sprite`
|
||||||
|
- proxy geocoding / directions / autocomplete
|
||||||
|
|
||||||
|
Điều này chỉ đúng khi frontend vẫn giữ kiến trúc hiện tại.
|
||||||
|
|
||||||
|
Nếu sau này frontend chuyển sang `map.setStyle(goongStyleJson)` trực tiếp, hãy đánh giá lại:
|
||||||
|
|
||||||
|
- `glyphs`
|
||||||
|
- `sprite`
|
||||||
|
|
||||||
|
vì khi đó chúng có thể trở thành dependency bắt buộc.
|
||||||
|
|
||||||
|
## 10. Gợi ý ngắn cho team BE
|
||||||
|
|
||||||
|
Nếu muốn làm ít rủi ro nhất:
|
||||||
|
|
||||||
|
1. làm proxy `assets/*.json`
|
||||||
|
2. rewrite `sources.*.url`
|
||||||
|
3. làm proxy `sources/*.json`
|
||||||
|
4. rewrite `tiles[]`
|
||||||
|
5. làm proxy generic cho tile
|
||||||
|
|
||||||
|
Nếu làm thiếu bước 4 hoặc 5 thì key vẫn có thể lộ ở request tile.
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Goong Map Web Structure
|
||||||
|
|
||||||
|
Nguồn JSON gốc được tải về tại:
|
||||||
|
|
||||||
|
- `FrontEndUser/tmp/goong-styles/goong_map_web.json`
|
||||||
|
|
||||||
|
File này là style vector/label đầy đủ hơn, phù hợp để dò:
|
||||||
|
|
||||||
|
- water và water labels
|
||||||
|
- boundary theo cấp
|
||||||
|
- place labels cho lịch sử
|
||||||
|
|
||||||
|
## Mermaid overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
ROOT[goong_map_web.json]
|
||||||
|
|
||||||
|
ROOT --> S1[source: base]
|
||||||
|
ROOT --> S2[source: composite]
|
||||||
|
|
||||||
|
S1 --> B1[source-layer: boundary]
|
||||||
|
S1 --> B2[source-layer: worldcountriespoints]
|
||||||
|
S1 --> B3[source-layer: worldnationalcapitals]
|
||||||
|
|
||||||
|
S2 --> C1[source-layer: riversandlakes]
|
||||||
|
S2 --> C2[source-layer: rivernames]
|
||||||
|
S2 --> C3[source-layer: lakenames]
|
||||||
|
S2 --> C4[source-layer: vietnam_administrator]
|
||||||
|
S2 --> C5[source-layer: streets_label]
|
||||||
|
|
||||||
|
B1 --> BL0[boundary-land-type-0 / type-0-bg]
|
||||||
|
B1 --> BL1[boundary-land-type-1 / type-1-bg]
|
||||||
|
B1 --> BL2[boundary-land-type-2 / type-2-bg]
|
||||||
|
|
||||||
|
B2 --> PC1[place-country-1]
|
||||||
|
B2 --> PC2[place-country-2]
|
||||||
|
|
||||||
|
B3 --> CAP0[place-city-capital]
|
||||||
|
|
||||||
|
C1 --> W1[water]
|
||||||
|
C1 --> W2[water-shadow]
|
||||||
|
|
||||||
|
C2 --> RN0[river-name-0]
|
||||||
|
C2 --> RN1[river-name-1]
|
||||||
|
C2 --> RN2[river-name-2]
|
||||||
|
|
||||||
|
C3 --> LN0[lake-name_priority_0]
|
||||||
|
C3 --> LN1[lake-name_priority_1]
|
||||||
|
C3 --> LN2[lake-name_priority_2]
|
||||||
|
|
||||||
|
C4 --> VA0[place-city-capital-vietnam]
|
||||||
|
C4 --> VA1[place-city1 / place-city2]
|
||||||
|
C4 --> VA2[place-town1 / place-town2]
|
||||||
|
C4 --> VA3[place-suburb / borough / neighbourhood]
|
||||||
|
C4 --> VA4[place-village]
|
||||||
|
|
||||||
|
C5 --> RD0[highway-name-minor]
|
||||||
|
C5 --> RD1[highway-name-medium]
|
||||||
|
C5 --> RD2[highway-name-major]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Boundary layers
|
||||||
|
|
||||||
|
Các layer boundary nổi bật:
|
||||||
|
|
||||||
|
- `boundary-land-type-0-bg`
|
||||||
|
- `boundary-land-type-0`
|
||||||
|
- `boundary-land-type-1-bg`
|
||||||
|
- `boundary-land-type-1`
|
||||||
|
- `boundary-land-type-2-bg`
|
||||||
|
- `boundary-land-type-2`
|
||||||
|
|
||||||
|
Minzoom quan sát được:
|
||||||
|
|
||||||
|
- `type-0`: từ zoom `1`
|
||||||
|
- `type-1`: từ zoom `5`
|
||||||
|
- `type-2-bg`: từ zoom `7`
|
||||||
|
- `type-2`: từ zoom `13`
|
||||||
|
|
||||||
|
Suy luận thực dụng:
|
||||||
|
|
||||||
|
- `type-0` có khả năng là biên giới quốc gia
|
||||||
|
- `type-1` có khả năng là cấp tỉnh/thành
|
||||||
|
- `type-2` có khả năng là cấp sâu hơn
|
||||||
|
|
||||||
|
## Water layers
|
||||||
|
|
||||||
|
Water fill:
|
||||||
|
|
||||||
|
- `water`
|
||||||
|
- `water-shadow`
|
||||||
|
|
||||||
|
Water labels:
|
||||||
|
|
||||||
|
- `river-name-0`
|
||||||
|
- `river-name-1`
|
||||||
|
- `river-name-2`
|
||||||
|
- `lake-name_priority_0`
|
||||||
|
- `lake-name_priority_1`
|
||||||
|
- `lake-name_priority_2`
|
||||||
|
|
||||||
|
## Place labels
|
||||||
|
|
||||||
|
Những label đáng quan tâm cho historical use:
|
||||||
|
|
||||||
|
- `place-country-1`
|
||||||
|
- `place-country-2`
|
||||||
|
- `place-city-capital`
|
||||||
|
- `place-city-capital-vietnam`
|
||||||
|
- `place-city1`
|
||||||
|
- `place-city2`
|
||||||
|
- `place-town1`
|
||||||
|
- `place-town2`
|
||||||
|
|
||||||
|
Những label dễ gây rối nếu bật nhiều:
|
||||||
|
|
||||||
|
- `highway-name-*`
|
||||||
|
- `place-suburb*`
|
||||||
|
- `place-neighbourhood*`
|
||||||
|
- `place-village`
|
||||||
|
|
||||||
|
## Gợi ý mapping cho UI
|
||||||
|
|
||||||
|
- `Country Borders` -> `boundary-land-type-0` + `boundary-land-type-0-bg`
|
||||||
|
- `Province Borders` -> `boundary-land-type-1` + `boundary-land-type-1-bg`
|
||||||
|
- `District Borders` -> `boundary-land-type-2` + `boundary-land-type-2-bg`
|
||||||
|
- `Country Labels` -> `place-country-*`, `place-city-capital*`, `place-city*`, `place-town*`
|
||||||
|
- `Rivers` -> `water`, `water-shadow`, `river-name-*`, `lake-name_*`
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
# Goong Proxy Backend Guide
|
||||||
|
|
||||||
|
Tài liệu này mô tả:
|
||||||
|
|
||||||
|
- luồng request thật của frontend hiện tại
|
||||||
|
- backend cần proxy chỗ nào
|
||||||
|
- backend cần rewrite chỗ nào
|
||||||
|
- trade-off hiệu suất nếu proxy/rewrite toàn bộ Goong
|
||||||
|
- khuyến nghị triển khai thực dụng cho team BE
|
||||||
|
|
||||||
|
Tài liệu liên quan:
|
||||||
|
|
||||||
|
- [goong_apis_in_use.md](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/doc/goong_apis_in_use.md)
|
||||||
|
- [goong_map_web_structure.md](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/doc/goong_map_web_structure.md)
|
||||||
|
- [goong_satellite_structure.md](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/doc/goong_satellite_structure.md)
|
||||||
|
|
||||||
|
Code liên quan:
|
||||||
|
|
||||||
|
- [config.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/config.ts:1)
|
||||||
|
- [tiles.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/api/tiles.ts:1)
|
||||||
|
- [useMapLayers.ts](/home/amoratran/wsp/ultimate-history-map/FrontEndUser/src/uhm/components/map/useMapLayers.ts:1)
|
||||||
|
|
||||||
|
## 1. Bối cảnh hiện tại
|
||||||
|
|
||||||
|
Frontend hiện tại không `setStyle(goongStyle)` trực tiếp cho MapLibre.
|
||||||
|
|
||||||
|
Thay vào đó:
|
||||||
|
|
||||||
|
1. FE tự `fetch()` style JSON của Goong
|
||||||
|
2. FE parse style JSON
|
||||||
|
3. FE lấy ra:
|
||||||
|
- raster source cho satellite
|
||||||
|
- selected vector sources/layers cho borders, labels, rivers
|
||||||
|
4. FE `addSource()` và `addLayer()` thủ công
|
||||||
|
5. MapLibre tự request tiếp `source.url`
|
||||||
|
6. Từ source manifest, MapLibre tự request tiếp các tile URLs trong `tiles[]`
|
||||||
|
|
||||||
|
Điểm quan trọng:
|
||||||
|
|
||||||
|
- browser có thể không chỉ gọi `assets/*.json`
|
||||||
|
- browser sẽ đi sâu thêm ít nhất 2 tầng:
|
||||||
|
- `sources/*.json`
|
||||||
|
- tile URLs trong `tiles[]`
|
||||||
|
|
||||||
|
## 2. Luồng request hiện tại
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant FE as Frontend
|
||||||
|
participant GL as MapLibre
|
||||||
|
participant GO as Goong
|
||||||
|
|
||||||
|
FE->>GO: GET assets/goong_satellite.json?api_key=...
|
||||||
|
FE->>GO: GET assets/goong_map_web.json?api_key=...
|
||||||
|
|
||||||
|
FE->>GL: addSource(raster/vector) + addLayer(...)
|
||||||
|
|
||||||
|
GL->>GO: GET sources/satellite.json?api_key=...
|
||||||
|
GL->>GO: GET sources/base.json?api_key=...
|
||||||
|
GL->>GO: GET sources/goong.json?api_key=...
|
||||||
|
|
||||||
|
GL->>GO: GET raster tile URLs from satellite tiles[]
|
||||||
|
GL->>GO: GET vector tile URLs from base tiles[]
|
||||||
|
GL->>GO: GET vector tile URLs from goong tiles[]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Mục tiêu của backend proxy
|
||||||
|
|
||||||
|
Nếu mục tiêu là:
|
||||||
|
|
||||||
|
- không lộ `api_key` ở browser
|
||||||
|
- vẫn giữ frontend hiện tại gần như nguyên
|
||||||
|
|
||||||
|
thì backend phải đảm bảo:
|
||||||
|
|
||||||
|
1. browser chỉ gọi domain BE
|
||||||
|
2. BE gọi Goong bằng key server-side
|
||||||
|
3. mọi URL Goong lồng bên trong JSON đều được rewrite về domain BE
|
||||||
|
|
||||||
|
Nếu thiếu bước 3:
|
||||||
|
|
||||||
|
- `api_key` vẫn có thể lộ ở request tầng sau
|
||||||
|
|
||||||
|
## 4. Những gì cần rewrite
|
||||||
|
|
||||||
|
### 4.1. Style JSON
|
||||||
|
|
||||||
|
Trong `goong_satellite.json` và `goong_map_web.json`, BE cần rewrite:
|
||||||
|
|
||||||
|
- `sources.*.url`
|
||||||
|
|
||||||
|
Ví dụ:
|
||||||
|
|
||||||
|
- từ `https://tiles.goong.io/sources/base.json?api_key=...`
|
||||||
|
- thành `/proxy/goong/sources/base.json`
|
||||||
|
|
||||||
|
### 4.2. Source manifests
|
||||||
|
|
||||||
|
Trong `sources/satellite.json`, `sources/base.json`, `sources/goong.json`, BE cần rewrite:
|
||||||
|
|
||||||
|
- mọi phần tử trong `tiles[]`
|
||||||
|
|
||||||
|
Ví dụ:
|
||||||
|
|
||||||
|
- từ `https://.../{z}/{x}/{y}...api_key=...`
|
||||||
|
- thành `/proxy/goong/tiles/...`
|
||||||
|
|
||||||
|
### 4.3. Những field còn phải để ý cho flow hiện tại
|
||||||
|
|
||||||
|
Với kiến trúc frontend hiện tại:
|
||||||
|
|
||||||
|
- `glyphs` đang được FE dùng qua proxy
|
||||||
|
- `sprite` hiện chưa dùng
|
||||||
|
|
||||||
|
Nghĩa là:
|
||||||
|
|
||||||
|
- BE **phải** proxy được `fonts/{fontstack}/{range}.pbf`
|
||||||
|
- BE hiện **chưa cần** proxy `sprite`
|
||||||
|
|
||||||
|
Nếu sau này FE chuyển sang `map.setStyle(goongStyleJson)` trực tiếp thì phải đánh giá lại `sprite` ngay.
|
||||||
|
|
||||||
|
## 5. Backend endpoint được khuyến nghị
|
||||||
|
|
||||||
|
### 5.1. Style endpoints
|
||||||
|
|
||||||
|
- `GET /proxy/goong/assets/goong_satellite.json`
|
||||||
|
- `GET /proxy/goong/assets/goong_map_web.json`
|
||||||
|
|
||||||
|
Nhiệm vụ:
|
||||||
|
|
||||||
|
- gọi upstream Goong bằng key server-side
|
||||||
|
- parse JSON
|
||||||
|
- rewrite `sources.*.url`
|
||||||
|
- trả JSON đã rewrite
|
||||||
|
|
||||||
|
### 5.2. Source endpoints
|
||||||
|
|
||||||
|
- `GET /proxy/goong/sources/satellite.json`
|
||||||
|
- `GET /proxy/goong/sources/base.json`
|
||||||
|
- `GET /proxy/goong/sources/goong.json`
|
||||||
|
|
||||||
|
Nhiệm vụ:
|
||||||
|
|
||||||
|
- gọi upstream Goong bằng key server-side
|
||||||
|
- parse JSON
|
||||||
|
- rewrite `tiles[]`
|
||||||
|
- giữ nguyên:
|
||||||
|
- `bounds`
|
||||||
|
- `minzoom`
|
||||||
|
- `maxzoom`
|
||||||
|
- `scheme`
|
||||||
|
- `tileSize`
|
||||||
|
- `attribution`
|
||||||
|
|
||||||
|
### 5.3. Tile endpoint
|
||||||
|
|
||||||
|
Gợi ý route generic:
|
||||||
|
|
||||||
|
- `GET /proxy/goong/tiles/*`
|
||||||
|
|
||||||
|
Nhiệm vụ:
|
||||||
|
|
||||||
|
- nhận tile request từ browser
|
||||||
|
- map sang upstream tile URL tương ứng
|
||||||
|
- gọi Goong bằng key server-side nếu upstream yêu cầu
|
||||||
|
- stream response về browser
|
||||||
|
|
||||||
|
Điểm quan trọng:
|
||||||
|
|
||||||
|
- tile response không nên parse lại
|
||||||
|
- tile response nên stream/pass-through
|
||||||
|
- giữ cache headers càng nhiều càng tốt
|
||||||
|
|
||||||
|
## 6. Luồng request sau khi proxy
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant FE as Frontend
|
||||||
|
participant GL as MapLibre
|
||||||
|
participant BE as Backend Proxy
|
||||||
|
participant GO as Goong
|
||||||
|
|
||||||
|
FE->>BE: GET /proxy/goong/assets/goong_satellite.json
|
||||||
|
FE->>BE: GET /proxy/goong/assets/goong_map_web.json
|
||||||
|
|
||||||
|
BE->>GO: fetch upstream style JSON
|
||||||
|
GO-->>BE: style JSON
|
||||||
|
BE-->>FE: rewritten style JSON
|
||||||
|
|
||||||
|
FE->>GL: addSource(raster/vector) + addLayer(...)
|
||||||
|
|
||||||
|
GL->>BE: GET /proxy/goong/sources/satellite.json
|
||||||
|
GL->>BE: GET /proxy/goong/sources/base.json
|
||||||
|
GL->>BE: GET /proxy/goong/sources/goong.json
|
||||||
|
|
||||||
|
BE->>GO: fetch upstream source manifests
|
||||||
|
GO-->>BE: source manifests
|
||||||
|
BE-->>GL: rewritten source manifests
|
||||||
|
|
||||||
|
GL->>BE: GET /proxy/goong/tiles/...
|
||||||
|
BE->>GO: fetch upstream tile
|
||||||
|
GO-->>BE: tile bytes
|
||||||
|
BE-->>GL: tile bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Trade-off hiệu suất
|
||||||
|
|
||||||
|
### 7.1. Rewrite JSON có chậm không?
|
||||||
|
|
||||||
|
Có overhead, nhưng **rất nhỏ** so với tile traffic.
|
||||||
|
|
||||||
|
JSON cần rewrite hiện tại chỉ gồm:
|
||||||
|
|
||||||
|
- 2 style JSON
|
||||||
|
- 3 source manifests
|
||||||
|
|
||||||
|
Những file này nhỏ, số lượng ít, và có thể cache rất mạnh.
|
||||||
|
|
||||||
|
Kết luận:
|
||||||
|
|
||||||
|
- rewrite JSON không phải bottleneck chính
|
||||||
|
|
||||||
|
### 7.2. Tile proxy mới là chỗ đắt
|
||||||
|
|
||||||
|
Chi phí hiệu suất chính nằm ở:
|
||||||
|
|
||||||
|
- mọi tile phải đi qua backend
|
||||||
|
- backend phải giữ thêm một hop mạng
|
||||||
|
- mất lợi thế gọi trực tiếp CDN của Goong từ browser
|
||||||
|
|
||||||
|
Các ảnh hưởng có thể thấy:
|
||||||
|
|
||||||
|
- tăng latency
|
||||||
|
- tăng bandwidth qua BE
|
||||||
|
- tăng CPU/memory nếu BE buffer response thay vì stream
|
||||||
|
- tăng load connection pool tới Goong
|
||||||
|
|
||||||
|
### 7.3. Nếu không rewrite tile URL
|
||||||
|
|
||||||
|
Nếu BE chỉ rewrite style/source JSON nhưng không rewrite `tiles[]`:
|
||||||
|
|
||||||
|
- browser vẫn gọi Goong trực tiếp ở bước tile
|
||||||
|
- `api_key` vẫn có thể lộ
|
||||||
|
|
||||||
|
Tức là:
|
||||||
|
|
||||||
|
- hiệu suất tốt hơn
|
||||||
|
- nhưng mục tiêu bảo mật key không đạt
|
||||||
|
|
||||||
|
## 8. Cách giảm thiểu impact hiệu suất
|
||||||
|
|
||||||
|
### 8.1. Cache rewritten JSON ở BE
|
||||||
|
|
||||||
|
Khuyến nghị:
|
||||||
|
|
||||||
|
- cache in-memory hoặc Redis cho:
|
||||||
|
- `goong_satellite.json`
|
||||||
|
- `goong_map_web.json`
|
||||||
|
- `sources/satellite.json`
|
||||||
|
- `sources/base.json`
|
||||||
|
- `sources/goong.json`
|
||||||
|
|
||||||
|
TTL có thể dài vì:
|
||||||
|
|
||||||
|
- style/source manifest không đổi liên tục
|
||||||
|
|
||||||
|
Tối ưu:
|
||||||
|
|
||||||
|
- chỉ rewrite một lần rồi reuse
|
||||||
|
|
||||||
|
### 8.2. Stream tile response
|
||||||
|
|
||||||
|
Cho tile route:
|
||||||
|
|
||||||
|
- không parse body
|
||||||
|
- không buffer toàn bộ file vào memory nếu không cần
|
||||||
|
- stream thẳng upstream -> client
|
||||||
|
|
||||||
|
### 8.3. Preserve cache headers
|
||||||
|
|
||||||
|
Với tile route, BE nên pass-through hoặc preserve:
|
||||||
|
|
||||||
|
- `Cache-Control`
|
||||||
|
- `ETag`
|
||||||
|
- `Last-Modified`
|
||||||
|
- `Content-Type`
|
||||||
|
|
||||||
|
Nếu BE/ngược phía CDN có cache tốt, impact sẽ giảm rất nhiều.
|
||||||
|
|
||||||
|
### 8.4. Dùng CDN/reverse proxy trước BE nếu có thể
|
||||||
|
|
||||||
|
Nếu production có CDN/nginx/edge cache:
|
||||||
|
|
||||||
|
- cache mạnh cho:
|
||||||
|
- rewritten style JSON
|
||||||
|
- rewritten source manifests
|
||||||
|
- tile responses
|
||||||
|
|
||||||
|
Điều này quan trọng hơn tối ưu code rewrite.
|
||||||
|
|
||||||
|
### 8.5. Đừng rewrite tile mỗi request theo kiểu string building phức tạp
|
||||||
|
|
||||||
|
Nên:
|
||||||
|
|
||||||
|
- rewrite `tiles[]` một lần ở source manifest
|
||||||
|
- tile route chỉ resolve path đơn giản và forward
|
||||||
|
|
||||||
|
Không nên:
|
||||||
|
|
||||||
|
- parse lại manifest ở mỗi tile request
|
||||||
|
|
||||||
|
## 9. Recommendation thực dụng
|
||||||
|
|
||||||
|
Nếu team BE muốn giải pháp cân bằng giữa bảo mật và hiệu suất:
|
||||||
|
|
||||||
|
### Option A. Full proxy, full rewrite
|
||||||
|
|
||||||
|
BE cover:
|
||||||
|
|
||||||
|
1. style JSON
|
||||||
|
2. source manifests
|
||||||
|
3. tiles
|
||||||
|
|
||||||
|
Ưu điểm:
|
||||||
|
|
||||||
|
- key không lộ ra browser
|
||||||
|
- FE không cần biết upstream Goong
|
||||||
|
|
||||||
|
Nhược điểm:
|
||||||
|
|
||||||
|
- BE chịu toàn bộ traffic tile
|
||||||
|
|
||||||
|
### Option B. Hybrid
|
||||||
|
|
||||||
|
BE cover:
|
||||||
|
|
||||||
|
1. style JSON
|
||||||
|
2. source manifests
|
||||||
|
|
||||||
|
Nhưng không rewrite `tiles[]`
|
||||||
|
|
||||||
|
Ưu điểm:
|
||||||
|
|
||||||
|
- BE nhẹ hơn
|
||||||
|
|
||||||
|
Nhược điểm:
|
||||||
|
|
||||||
|
- key vẫn lộ ở tile request
|
||||||
|
|
||||||
|
Kết luận:
|
||||||
|
|
||||||
|
- nếu ưu tiên bảo mật key thật sự: dùng **Option A**
|
||||||
|
- nếu ưu tiên hiệu suất hơn và chấp nhận domain restrictions của Goong: dùng **Option B**
|
||||||
|
|
||||||
|
## 10. Recommendation cho codebase hiện tại
|
||||||
|
|
||||||
|
Với frontend hiện tại, hướng hợp lý nhất là:
|
||||||
|
|
||||||
|
1. giữ nguyên FE logic parse style/source như hiện nay
|
||||||
|
2. chuyển các URL Goong ở `config.ts` sang endpoint nội bộ BE
|
||||||
|
3. để BE rewrite:
|
||||||
|
- `sources.*.url`
|
||||||
|
- `tiles[]`
|
||||||
|
4. để BE stream tile response
|
||||||
|
5. cache rewritten JSON ở BE
|
||||||
|
|
||||||
|
Nói ngắn:
|
||||||
|
|
||||||
|
- rewrite JSON: nên làm
|
||||||
|
- rewrite tile URLs: bắt buộc nếu muốn giấu key
|
||||||
|
- proxy tile: phần tốn hiệu suất nhất
|
||||||
|
- muốn bù hiệu suất: phải dùng cache/stream/CDN tốt
|
||||||
|
|
||||||
|
## 11. Checklist cho team BE
|
||||||
|
|
||||||
|
1. Tạo route proxy cho 2 style JSON
|
||||||
|
2. Tạo route proxy cho 3 source manifests
|
||||||
|
3. Rewrite `sources.*.url` trong style JSON
|
||||||
|
4. Rewrite `tiles[]` trong source manifests
|
||||||
|
5. Tạo route proxy tile generic
|
||||||
|
6. Stream tile response
|
||||||
|
7. Preserve cache headers
|
||||||
|
8. Cache rewritten JSON
|
||||||
|
9. Kiểm tra browser không còn request trực tiếp `tiles.goong.io`
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Goong Satellite Structure
|
||||||
|
|
||||||
|
Nguồn JSON gốc được tải về tại:
|
||||||
|
|
||||||
|
- `FrontEndUser/tmp/goong-styles/goong_satellite.json`
|
||||||
|
|
||||||
|
File này là style satellite. Nó vẫn có boundary và labels, nhưng ít lớp nước hơn `goong_map_web.json`.
|
||||||
|
|
||||||
|
## Mermaid overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
ROOT[goong_satellite.json]
|
||||||
|
|
||||||
|
ROOT --> S0[source: satellite]
|
||||||
|
ROOT --> S1[source: base]
|
||||||
|
ROOT --> S2[source: composite]
|
||||||
|
|
||||||
|
S1 --> B1[source-layer: boundary]
|
||||||
|
S1 --> B2[source-layer: worldcountriespoints]
|
||||||
|
S1 --> B3[source-layer: worldnationalcapitals]
|
||||||
|
|
||||||
|
S2 --> C1[source-layer: vietnam_administrator]
|
||||||
|
S2 --> C2[source-layer: streets_label]
|
||||||
|
|
||||||
|
B1 --> BL0[boundary-land-type-0 / type-0-bg]
|
||||||
|
B1 --> BL1[boundary-land-type-1 / type-1-bg]
|
||||||
|
B1 --> BL2[boundary-land-type-2 / type-2-bg]
|
||||||
|
|
||||||
|
B2 --> PC1[place-country-1]
|
||||||
|
B2 --> PC2[place-country-2]
|
||||||
|
|
||||||
|
B3 --> CAP0[place-city-capital]
|
||||||
|
|
||||||
|
C1 --> VA0[place-city-capital-vietnam]
|
||||||
|
C1 --> VA1[place-city1 / place-city2]
|
||||||
|
C1 --> VA2[place-town1 / place-town2]
|
||||||
|
C1 --> VA3[place-suburb / borough / neighbourhood]
|
||||||
|
C1 --> VA4[place-village]
|
||||||
|
|
||||||
|
C2 --> RD0[highway-name-minor]
|
||||||
|
C2 --> RD1[highway-name-medium]
|
||||||
|
C2 --> RD2[highway-name-major]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Boundary layers
|
||||||
|
|
||||||
|
Các layer boundary nổi bật:
|
||||||
|
|
||||||
|
- `boundary-land-type-0-bg`
|
||||||
|
- `boundary-land-type-0`
|
||||||
|
- `boundary-land-type-1-bg`
|
||||||
|
- `boundary-land-type-1`
|
||||||
|
- `boundary-land-type-2-bg`
|
||||||
|
- `boundary-land-type-2`
|
||||||
|
|
||||||
|
Minzoom quan sát được:
|
||||||
|
|
||||||
|
- `type-0`: từ zoom `1`
|
||||||
|
- `type-1`: từ zoom `5`
|
||||||
|
- `type-2-bg`: từ zoom `7`
|
||||||
|
- `type-2`: từ zoom `7`
|
||||||
|
|
||||||
|
## Place labels
|
||||||
|
|
||||||
|
Labels hữu ích:
|
||||||
|
|
||||||
|
- `place-country-1`
|
||||||
|
- `place-country-2`
|
||||||
|
- `place-city-capital`
|
||||||
|
- `place-city-capital-vietnam`
|
||||||
|
- `place-city1`
|
||||||
|
- `place-city2`
|
||||||
|
- `place-town1`
|
||||||
|
- `place-town2`
|
||||||
|
|
||||||
|
Labels dễ gây rối:
|
||||||
|
|
||||||
|
- `highway-name-*`
|
||||||
|
- `place-suburb*`
|
||||||
|
- `place-neighbourhood*`
|
||||||
|
- `place-village`
|
||||||
|
|
||||||
|
## Khác biệt thực dụng so với goong_map_web
|
||||||
|
|
||||||
|
- Có `source: satellite`
|
||||||
|
- Boundary vẫn hiện diện rõ
|
||||||
|
- Labels hành chính vẫn có
|
||||||
|
- Không lộ ra nhóm water chi tiết rõ như `goong_map_web`
|
||||||
|
- Phù hợp làm raster/satellite nền hơn là style để dò water layers
|
||||||
|
|
||||||
|
## Gợi ý dùng thực tế
|
||||||
|
|
||||||
|
- Dùng `goong_satellite.json` cho nền satellite
|
||||||
|
- Dùng `goong_map_web.json` để dò:
|
||||||
|
- water
|
||||||
|
- water labels
|
||||||
|
- boundary theo cấp
|
||||||
|
- labels hành chính
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
DEFAULT_BACKGROUND_LAYER_VISIBILITY,
|
DEFAULT_BACKGROUND_LAYER_VISIBILITY,
|
||||||
} from "@/uhm/lib/map/styles/backgroundLayers";
|
} from "@/uhm/lib/map/styles/backgroundLayers";
|
||||||
|
|
||||||
const BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY = "uhm.backgroundLayerVisibility.v1";
|
const BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY = "uhm.backgroundLayerVisibility.v2";
|
||||||
|
|
||||||
export function loadBackgroundLayerVisibilityFromStorage(): BackgroundLayerVisibility {
|
export function loadBackgroundLayerVisibilityFromStorage(): BackgroundLayerVisibility {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
@@ -57,4 +57,3 @@ function normalizeBackgroundLayerVisibility(raw: unknown): BackgroundLayerVisibi
|
|||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export const MAP_MAX_ZOOM = 10;
|
|||||||
|
|
||||||
export const RASTER_BASE_SOURCE_ID = "rasterBase";
|
export const RASTER_BASE_SOURCE_ID = "rasterBase";
|
||||||
export const RASTER_BASE_LAYER_ID = "raster-base-layer";
|
export const RASTER_BASE_LAYER_ID = "raster-base-layer";
|
||||||
export const RASTER_BASE_INSERT_BEFORE_LAYER_ID = "graticules-line";
|
|
||||||
|
|
||||||
export const PATH_ARROW_SOURCE_ID = "path-arrow-shapes";
|
export const PATH_ARROW_SOURCE_ID = "path-arrow-shapes";
|
||||||
export const POLYGON_LABEL_SOURCE_ID = "polygon-labels";
|
export const POLYGON_LABEL_SOURCE_ID = "polygon-labels";
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
export const BACKGROUND_LAYER_OPTIONS = [
|
export const BACKGROUND_LAYER_OPTIONS = [
|
||||||
{ id: "raster-base-layer", label: "Raster" },
|
{ id: "raster-base-layer", label: "Raster" },
|
||||||
{ id: "graticules-line", label: "Graticules" },
|
|
||||||
{ id: "land", label: "Land" },
|
|
||||||
{ id: "bg-countries-fill", label: "Countries" },
|
|
||||||
{ id: "bg-country-borders-line", label: "Country Borders" },
|
{ id: "bg-country-borders-line", label: "Country Borders" },
|
||||||
|
{ id: "bg-province-borders-line", label: "Province Borders" },
|
||||||
|
{ id: "bg-district-borders-line", label: "District Borders" },
|
||||||
{ id: "country-labels", label: "Country Labels" },
|
{ id: "country-labels", label: "Country Labels" },
|
||||||
{ id: "regions-line", label: "Regions" },
|
|
||||||
{ id: "lakes-fill", label: "Lakes" },
|
|
||||||
{ id: "rivers-line", label: "Rivers" },
|
{ id: "rivers-line", label: "Rivers" },
|
||||||
{ id: "geolines-line", label: "Geolines" },
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type BackgroundLayerId = (typeof BACKGROUND_LAYER_OPTIONS)[number]["id"];
|
export type BackgroundLayerId = (typeof BACKGROUND_LAYER_OPTIONS)[number]["id"];
|
||||||
export type BackgroundLayerVisibility = Record<BackgroundLayerId, boolean>;
|
export type BackgroundLayerVisibility = Record<BackgroundLayerId, boolean>;
|
||||||
|
|
||||||
// Tạo map visibility mặc định cho toàn bộ background layers.
|
export const DEFAULT_BACKGROUND_LAYER_VISIBILITY: BackgroundLayerVisibility = {
|
||||||
function buildBackgroundLayerVisibility(value: boolean): BackgroundLayerVisibility {
|
"raster-base-layer": true,
|
||||||
return BACKGROUND_LAYER_OPTIONS.reduce((acc, option) => {
|
"bg-country-borders-line": true,
|
||||||
acc[option.id] = value;
|
"bg-province-borders-line": false,
|
||||||
return acc;
|
"bg-district-borders-line": false,
|
||||||
}, {} as BackgroundLayerVisibility);
|
"country-labels": true,
|
||||||
}
|
"rivers-line": true,
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_BACKGROUND_LAYER_VISIBILITY =
|
export const HIDDEN_BACKGROUND_LAYER_VISIBILITY: BackgroundLayerVisibility = {
|
||||||
buildBackgroundLayerVisibility(true);
|
"raster-base-layer": false,
|
||||||
|
"bg-country-borders-line": false,
|
||||||
export const HIDDEN_BACKGROUND_LAYER_VISIBILITY =
|
"bg-province-borders-line": false,
|
||||||
buildBackgroundLayerVisibility(false);
|
"bg-district-borders-line": false,
|
||||||
|
"country-labels": false,
|
||||||
|
"rivers-line": false,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
||||||
|
import { MAP_TEXT_FONT_STACK } from "./textFonts";
|
||||||
|
|
||||||
const LINE_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
const LINE_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
|
||||||
"any",
|
"any",
|
||||||
@@ -14,6 +15,7 @@ export function getLineLabelLayers(sourceId: string): LayerSpecification[] {
|
|||||||
source: sourceId,
|
source: sourceId,
|
||||||
filter: ["all", LINE_GEOMETRY_FILTER, ["!=", ["coalesce", ["get", "line_label"], ""], ""]],
|
filter: ["all", LINE_GEOMETRY_FILTER, ["!=", ["coalesce", ["get", "line_label"], ""], ""]],
|
||||||
layout: {
|
layout: {
|
||||||
|
"text-font": [...MAP_TEXT_FONT_STACK],
|
||||||
"symbol-placement": "line",
|
"symbol-placement": "line",
|
||||||
"symbol-spacing": 280,
|
"symbol-spacing": 280,
|
||||||
"text-field": ["coalesce", ["get", "line_label"], ""],
|
"text-field": ["coalesce", ["get", "line_label"], ""],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
import maplibregl, { LayerSpecification } from "maplibre-gl";
|
||||||
|
import { MAP_EMPHASIS_TEXT_FONT_STACK } from "./textFonts";
|
||||||
|
|
||||||
export const POINT_GEOTYPE_IDS = [
|
export const POINT_GEOTYPE_IDS = [
|
||||||
"person_birthplace",
|
"person_birthplace",
|
||||||
@@ -168,6 +169,7 @@ export function buildPointGeotypeLayers(
|
|||||||
"icon-allow-overlap": true,
|
"icon-allow-overlap": true,
|
||||||
"icon-ignore-placement": true,
|
"icon-ignore-placement": true,
|
||||||
"symbol-placement": "point",
|
"symbol-placement": "point",
|
||||||
|
"text-font": [...MAP_EMPHASIS_TEXT_FONT_STACK],
|
||||||
"text-field": ["coalesce", ["get", "point_label"], ""],
|
"text-field": ["coalesce", ["get", "point_label"], ""],
|
||||||
"text-size": [
|
"text-size": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LayerSpecification } from "maplibre-gl";
|
import { LayerSpecification } from "maplibre-gl";
|
||||||
|
import { MAP_TEXT_FONT_STACK } from "./textFonts";
|
||||||
|
|
||||||
export function getPolygonLabelLayers(sourceId: string): LayerSpecification[] {
|
export function getPolygonLabelLayers(sourceId: string): LayerSpecification[] {
|
||||||
return [
|
return [
|
||||||
@@ -7,6 +8,7 @@ export function getPolygonLabelLayers(sourceId: string): LayerSpecification[] {
|
|||||||
type: "symbol",
|
type: "symbol",
|
||||||
source: sourceId,
|
source: sourceId,
|
||||||
layout: {
|
layout: {
|
||||||
|
"text-font": [...MAP_TEXT_FONT_STACK],
|
||||||
"text-field": ["coalesce", ["get", "polygon_label"], ""],
|
"text-field": ["coalesce", ["get", "polygon_label"], ""],
|
||||||
"text-size": [
|
"text-size": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const MAP_TEXT_FONT_STACK = ["Roboto Regular"] as const;
|
||||||
|
export const MAP_EMPHASIS_TEXT_FONT_STACK = ["Roboto Medium"] as const;
|
||||||
|
export const GOONG_LABEL_FALLBACK_FONT_STACK = ["Roboto Regular"] as const;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user