diff --git a/miniprogram/engine/layer/tileLayer.ts b/miniprogram/engine/layer/tileLayer.ts index 54dacbe..408924c 100644 --- a/miniprogram/engine/layer/tileLayer.ts +++ b/miniprogram/engine/layer/tileLayer.ts @@ -1,4 +1,5 @@ import { createTileGrid, type TileItem } from '../../utils/tile' +import { isTileWithinBounds } from '../../utils/remoteMapConfig' import { getTileSizePx, type CameraState } from '../camera/camera' import { type MapScene } from '../renderer/mapRenderer' import { type TileStore } from '../tile/tileStore' @@ -52,7 +53,7 @@ export class TileLayer implements MapLayer { viewportHeight: scene.viewportHeight, tileSize, overdraw: scene.overdraw, - }) + }).filter((tile) => isTileWithinBounds(scene.tileBoundsByZoom, scene.zoom, tile.x, tile.y)) } tileStore.queueVisibleTiles(this.cachedTiles, scene, gridKey) @@ -122,3 +123,4 @@ export class TileLayer implements MapLayer { } } } + diff --git a/miniprogram/engine/map/mapEngine.ts b/miniprogram/engine/map/mapEngine.ts index 5109ba5..0028a34 100644 --- a/miniprogram/engine/map/mapEngine.ts +++ b/miniprogram/engine/map/mapEngine.ts @@ -3,12 +3,13 @@ import { CompassHeadingController } from '../sensor/compassHeadingController' import { WebGLMapRenderer } from '../renderer/webglMapRenderer' import { type MapRendererStats } from '../renderer/mapRenderer' import { worldTileToLonLat, type LonLatPoint } from '../../utils/projection' +import { type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig' const RENDER_MODE = 'Single WebGL Pipeline' const PROJECTION_MODE = 'WGS84 -> WorldTile -> Camera -> Screen' const MAP_NORTH_OFFSET_DEG = 0 -const MAGNETIC_DECLINATION_DEG = -6.91 -const MAGNETIC_DECLINATION_TEXT = '6.91° W' +let MAGNETIC_DECLINATION_DEG = -6.91 +let MAGNETIC_DECLINATION_TEXT = '6.91° W' const MIN_ZOOM = 15 const MAX_ZOOM = 20 const DEFAULT_ZOOM = 17 @@ -70,6 +71,7 @@ export interface MapEngineViewState { mapReady: boolean mapReadyText: string mapName: string + configStatusText: string zoom: number rotationDeg: number rotationText: string @@ -119,6 +121,7 @@ const VIEW_SYNC_KEYS: Array = [ 'mapReady', 'mapReadyText', 'mapName', + 'configStatusText', 'zoom', 'rotationDeg', 'rotationText', @@ -383,6 +386,12 @@ export class MapEngine { autoRotateSourceMode: AutoRotateSourceMode autoRotateCalibrationOffsetDeg: number | null autoRotateCalibrationPending: boolean + minZoom: number + maxZoom: number + defaultZoom: number + defaultCenterTileX: number + defaultCenterTileY: number + tileBoundsByZoom: Record | null constructor(buildVersion: string, callbacks: MapEngineCallbacks) { this.buildVersion = buildVersion @@ -405,6 +414,12 @@ export class MapEngine { this.handleCompassError(message) }, }) + this.minZoom = MIN_ZOOM + this.maxZoom = MAX_ZOOM + this.defaultZoom = DEFAULT_ZOOM + this.defaultCenterTileX = DEFAULT_CENTER_TILE_X + this.defaultCenterTileY = DEFAULT_CENTER_TILE_Y + this.tileBoundsByZoom = null this.state = { buildVersion: this.buildVersion, renderMode: RENDER_MODE, @@ -412,6 +427,7 @@ export class MapEngine { mapReady: false, mapReadyText: 'BOOTING', mapName: 'LCX 测试地图', + configStatusText: '远程配置待加载', zoom: DEFAULT_ZOOM, rotationDeg: 0, rotationText: formatRotationText(0), @@ -526,6 +542,55 @@ export class MapEngine { this.compassController.start() } + applyRemoteMapConfig(config: RemoteMapConfig): void { + MAGNETIC_DECLINATION_DEG = config.magneticDeclinationDeg + MAGNETIC_DECLINATION_TEXT = config.magneticDeclinationText + this.minZoom = config.minZoom + this.maxZoom = config.maxZoom + this.defaultZoom = config.defaultZoom + this.defaultCenterTileX = config.initialCenterTileX + this.defaultCenterTileY = config.initialCenterTileY + this.tileBoundsByZoom = config.tileBoundsByZoom + + const statePatch: Partial = { + configStatusText: '远程配置已载入', + projectionMode: config.projectionModeText, + tileSource: config.tileSource, + sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg === null ? null : getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)), + compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode), + northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode), + northReferenceText: formatNorthReferenceText(this.northReferenceMode), + compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg), + } + + if (!this.state.stageWidth || !this.state.stageHeight) { + this.setState({ + ...statePatch, + zoom: this.defaultZoom, + centerTileX: this.defaultCenterTileX, + centerTileY: this.defaultCenterTileY, + centerText: buildCenterText(this.defaultZoom, this.defaultCenterTileX, this.defaultCenterTileY), + statusText: `远程地图配置已载入 (${this.buildVersion})`, + }, true) + return + } + + this.commitViewport({ + ...statePatch, + zoom: this.defaultZoom, + centerTileX: this.defaultCenterTileX, + centerTileY: this.defaultCenterTileY, + tileTranslateX: 0, + tileTranslateY: 0, + }, `远程地图配置已载入 (${this.buildVersion})`, true, () => { + this.resetPreviewState() + this.syncRenderer() + if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) { + this.scheduleAutoRotate() + } + }) + } + handleTouchStart(event: WechatMiniprogram.TouchEvent): void { this.clearInertiaTimer() this.clearPreviewResetTimer() @@ -696,9 +761,9 @@ export class MapEngine { this.renderer.setAnimationPaused(false) this.commitViewport( { - zoom: DEFAULT_ZOOM, - centerTileX: DEFAULT_CENTER_TILE_X, - centerTileY: DEFAULT_CENTER_TILE_Y, + zoom: this.defaultZoom, + centerTileX: this.defaultCenterTileX, + centerTileY: this.defaultCenterTileY, tileTranslateX: 0, tileTranslateY: 0, }, @@ -1196,6 +1261,7 @@ export class MapEngine { zoom: this.state.zoom, centerTileX: this.state.centerTileX, centerTileY: this.state.centerTileY, + tileBoundsByZoom: this.tileBoundsByZoom, viewportWidth: this.state.stageWidth, viewportHeight: this.state.stageHeight, visibleColumns: DESIRED_VISIBLE_COLUMNS, @@ -1482,7 +1548,7 @@ export class MapEngine { } zoomAroundPoint(zoomDelta: number, stageX: number, stageY: number, residualScale: number): void { - const nextZoom = clamp(this.state.zoom + zoomDelta, MIN_ZOOM, MAX_ZOOM) + const nextZoom = clamp(this.state.zoom + zoomDelta, this.minZoom, this.maxZoom) const appliedDelta = nextZoom - this.state.zoom if (!appliedDelta) { this.animatePreviewToRest() @@ -1584,3 +1650,4 @@ export class MapEngine { + diff --git a/miniprogram/engine/renderer/mapRenderer.ts b/miniprogram/engine/renderer/mapRenderer.ts index 8679794..fba7a9c 100644 --- a/miniprogram/engine/renderer/mapRenderer.ts +++ b/miniprogram/engine/renderer/mapRenderer.ts @@ -1,12 +1,14 @@ import { type CameraState } from '../camera/camera' import { type TileStoreStats } from '../tile/tileStore' import { type LonLatPoint } from '../../utils/projection' +import { type TileZoomBounds } from '../../utils/remoteMapConfig' export interface MapScene { tileSource: string zoom: number centerTileX: number centerTileY: number + tileBoundsByZoom: Record | null viewportWidth: number viewportHeight: number visibleColumns: number @@ -42,3 +44,5 @@ export function buildCamera(scene: MapScene): CameraState { rotationRad: scene.rotationRad, } } + + diff --git a/miniprogram/engine/tile/tileStore.ts b/miniprogram/engine/tile/tileStore.ts index 23365d4..ef052ee 100644 --- a/miniprogram/engine/tile/tileStore.ts +++ b/miniprogram/engine/tile/tileStore.ts @@ -1,4 +1,5 @@ import { buildTileUrl, type TileItem } from '../../utils/tile' +import { isTileWithinBounds, type TileZoomBounds } from '../../utils/remoteMapConfig' import { getTilePersistentCache, type TilePersistentCache } from '../renderer/tilePersistentCache' const MAX_PARENT_FALLBACK_DEPTH = 2 @@ -51,6 +52,7 @@ export interface TileStoreScene { viewportHeight: number translateX: number translateY: number + tileBoundsByZoom: Record | null } export interface TileStoreCallbacks { @@ -241,6 +243,9 @@ export class TileStore { const scale = Math.pow(2, depth) const fallbackX = Math.floor(tile.x / scale) const fallbackY = Math.floor(tile.y / scale) + if (!isTileWithinBounds(scene.tileBoundsByZoom, fallbackZoom, fallbackX, fallbackY)) { + continue + } const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY) const fallbackPriority = priority / (depth + 1) const existingPriority = parentPriorityMap.get(fallbackUrl) @@ -486,7 +491,10 @@ export class TileStore { const scale = Math.pow(2, depth) const fallbackX = Math.floor(tile.x / scale) const fallbackY = Math.floor(tile.y / scale) - const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY) + if (!isTileWithinBounds(scene.tileBoundsByZoom, fallbackZoom, fallbackX, fallbackY)) { + continue + } + const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY) const fallbackEntry = this.tileCache.get(fallbackUrl) if (fallbackEntry && fallbackEntry.status === 'ready' && fallbackEntry.image) { @@ -540,6 +548,9 @@ export class TileStore { for (let offsetX = 0; offsetX < division; offsetX += 1) { const childX = tile.x * division + offsetX const childY = tile.y * division + offsetY + if (!isTileWithinBounds(scene.tileBoundsByZoom, childZoom, childX, childY)) { + continue + } const childUrl = buildTileUrl(scene.tileSource, childZoom, childX, childY) const childEntry = this.tileCache.get(childUrl) if (!childEntry || childEntry.status !== 'ready' || !childEntry.image) { @@ -565,3 +576,5 @@ export class TileStore { return null } } + + diff --git a/miniprogram/pages/map/map.ts b/miniprogram/pages/map/map.ts index a238cb2..3b34fff 100644 --- a/miniprogram/pages/map/map.ts +++ b/miniprogram/pages/map/map.ts @@ -1,10 +1,12 @@ import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine' +import { loadRemoteMapConfig } from '../../utils/remoteMapConfig' type MapPageData = MapEngineViewState & { showDebugPanel: boolean } -const INTERNAL_BUILD_VERSION = 'map-build-75' +const INTERNAL_BUILD_VERSION = 'map-build-82' +const REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/qyds-001/game.json' let mapEngine: MapEngine | null = null @@ -36,6 +38,7 @@ Page({ onReady() { this.measureStageAndCanvas() + this.loadMapConfigFromRemote() }, onUnload() { @@ -45,6 +48,33 @@ Page({ } }, + loadMapConfigFromRemote() { + const currentEngine = mapEngine + if (!currentEngine) { + return + } + + loadRemoteMapConfig(REMOTE_GAME_CONFIG_URL) + .then((config) => { + if (mapEngine !== currentEngine) { + return + } + + currentEngine.applyRemoteMapConfig(config) + }) + .catch((error) => { + if (mapEngine !== currentEngine) { + return + } + + const errorMessage = error && error.message ? error.message : '未知错误' + this.setData({ + configStatusText: `载入失败: ${errorMessage}`, + statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`, + }) + }) + }, + measureStageAndCanvas() { const page = this const applyStage = (rawRect?: Partial) => { @@ -179,6 +209,18 @@ Page({ + + + + + + + + + + + + diff --git a/miniprogram/pages/map/map.wxml b/miniprogram/pages/map/map.wxml index 448190f..8d18eb3 100644 --- a/miniprogram/pages/map/map.wxml +++ b/miniprogram/pages/map/map.wxml @@ -49,6 +49,14 @@ + + Build + {{buildVersion}} + + + Config + {{configStatusText}} + Heading Mode {{orientationModeText}} @@ -73,16 +81,11 @@ Status {{statusText}} - {{showDebugPanel ? '隐藏调试' : '查看调试'}} - - Build - {{buildVersion}} - Renderer {{renderMode}} @@ -161,3 +164,5 @@ + + diff --git a/miniprogram/utils/remoteMapConfig.ts b/miniprogram/utils/remoteMapConfig.ts new file mode 100644 index 0000000..4c9814a --- /dev/null +++ b/miniprogram/utils/remoteMapConfig.ts @@ -0,0 +1,404 @@ +import { lonLatToWorldTile, webMercatorToLonLat, type LonLatPoint } from './projection' + +export interface TileZoomBounds { + minX: number + maxX: number + minY: number + maxY: number +} + +export interface RemoteMapConfig { + tileSource: string + minZoom: number + maxZoom: number + defaultZoom: number + initialCenterTileX: number + initialCenterTileY: number + projection: string + projectionModeText: string + magneticDeclinationDeg: number + magneticDeclinationText: string + tileFormat: string + tileSize: number + bounds: [number, number, number, number] | null + tileBoundsByZoom: Record + mapMetaUrl: string + mapRootUrl: string +} + +interface ParsedGameConfig { + mapRoot: string + mapMeta: string + declinationDeg: number +} + +interface ParsedMapMeta { + tileSize: number + minZoom: number + maxZoom: number + projection: string + tileFormat: string + tilePathTemplate: string + bounds: [number, number, number, number] | null +} + +function requestTextViaRequest(url: string): Promise { + return new Promise((resolve, reject) => { + wx.request({ + url, + method: 'GET', + responseType: 'text' as any, + success: (response) => { + if (response.statusCode !== 200) { + reject(new Error(`request失败: ${response.statusCode} ${url}`)) + return + } + + if (typeof response.data === 'string') { + resolve(response.data) + return + } + + resolve(JSON.stringify(response.data)) + }, + fail: () => { + reject(new Error(`request失败: ${url}`)) + }, + }) + }) +} + +function requestTextViaDownload(url: string): Promise { + return new Promise((resolve, reject) => { + const fileSystemManager = wx.getFileSystemManager() + wx.downloadFile({ + url, + success: (response) => { + if (response.statusCode !== 200 || !response.tempFilePath) { + reject(new Error(`download失败: ${response.statusCode} ${url}`)) + return + } + + fileSystemManager.readFile({ + filePath: response.tempFilePath, + encoding: 'utf8', + success: (readResult) => { + if (typeof readResult.data === 'string') { + resolve(readResult.data) + return + } + + reject(new Error(`read失败: ${url}`)) + }, + fail: () => { + reject(new Error(`read失败: ${url}`)) + }, + }) + }, + fail: () => { + reject(new Error(`download失败: ${url}`)) + }, + }) + }) +} + +async function requestText(url: string): Promise { + try { + return await requestTextViaRequest(url) + } catch (requestError) { + try { + return await requestTextViaDownload(url) + } catch (downloadError) { + const requestMessage = requestError instanceof Error ? requestError.message : 'request失败' + const downloadMessage = downloadError instanceof Error ? downloadError.message : 'download失败' + throw new Error(`${requestMessage}; ${downloadMessage}`) + } + } +} + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)) +} + +function resolveUrl(baseUrl: string, relativePath: string): string { + if (/^https?:\/\//i.test(relativePath)) { + return relativePath + } + + const originMatch = baseUrl.match(/^(https?:\/\/[^/]+)/i) + const origin = originMatch ? originMatch[1] : '' + if (relativePath.startsWith('/')) { + return `${origin}${relativePath}` + } + + const baseDir = baseUrl.slice(0, baseUrl.lastIndexOf('/') + 1) + const normalizedRelativePath = relativePath.replace(/^\.\//, '') + return `${baseDir}${normalizedRelativePath}` +} + +function formatDeclinationText(declinationDeg: number): string { + const suffix = declinationDeg < 0 ? 'W' : 'E' + return `${Math.abs(declinationDeg).toFixed(2)}° ${suffix}` +} + +function parseDeclinationValue(rawValue: unknown): number { + const numericValue = Number(rawValue) + return Number.isFinite(numericValue) ? -Math.abs(numericValue) : -6.91 +} + +function parseGameConfigFromJson(text: string): ParsedGameConfig { + let parsed: Record + try { + parsed = JSON.parse(text) + } catch { + throw new Error('game.json 解析失败') + } + + const normalized: Record = {} + const keys = Object.keys(parsed) + for (const key of keys) { + normalized[key.toLowerCase()] = parsed[key] + } + + const mapRoot = typeof normalized.map === 'string' ? normalized.map : '' + const mapMeta = typeof normalized.mapmeta === 'string' ? normalized.mapmeta : '' + if (!mapRoot || !mapMeta) { + throw new Error('game.json 缺少 map 或 mapmeta 字段') + } + + return { + mapRoot, + mapMeta, + declinationDeg: parseDeclinationValue(normalized.declination), + } +} + +function parseGameConfigFromYaml(text: string): ParsedGameConfig { + const config: Record = {} + const lines = text.split(/\r?\n/) + + for (const rawLine of lines) { + const line = rawLine.trim() + if (!line || line.startsWith('#')) { + continue + } + + const match = line.match(/^([A-Za-z0-9_-]+)\s*(?:=|:)\s*(.+)$/) + if (!match) { + continue + } + + config[match[1].trim().toLowerCase()] = match[2].trim() + } + + const mapRoot = config.map + const mapMeta = config.mapmeta + if (!mapRoot || !mapMeta) { + throw new Error('game.yaml 缺少 map 或 mapmeta 字段') + } + + return { + mapRoot, + mapMeta, + declinationDeg: parseDeclinationValue(config.declination), + } +} + +function parseGameConfig(text: string, gameConfigUrl: string): ParsedGameConfig { + const trimmedText = text.trim() + const isJson = + trimmedText.startsWith('{') || + trimmedText.startsWith('[') || + /\.json(?:[?#].*)?$/i.test(gameConfigUrl) + + return isJson ? parseGameConfigFromJson(trimmedText) : parseGameConfigFromYaml(trimmedText) +} + +function extractStringField(text: string, key: string): string | null { + const pattern = new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`) + const match = text.match(pattern) + return match ? match[1] : null +} + +function extractNumberField(text: string, key: string): number | null { + const pattern = new RegExp(`"${key}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`) + const match = text.match(pattern) + if (!match) { + return null + } + + const value = Number(match[1]) + return Number.isFinite(value) ? value : null +} + +function extractNumberArrayField(text: string, key: string): number[] | null { + const pattern = new RegExp(`"${key}"\\s*:\\s*\\[([^\\]]+)\\]`) + const match = text.match(pattern) + if (!match) { + return null + } + + const numberMatches = match[1].match(/-?\d+(?:\.\d+)?/g) + if (!numberMatches || !numberMatches.length) { + return null + } + + const values = numberMatches + .map((item) => Number(item)) + .filter((item) => Number.isFinite(item)) + + return values.length ? values : null +} + +function parseMapMeta(text: string): ParsedMapMeta { + const tileSizeField = extractNumberField(text, 'tileSize') + const tileSize = tileSizeField === null ? 256 : tileSizeField + const minZoom = extractNumberField(text, 'minZoom') + const maxZoom = extractNumberField(text, 'maxZoom') + const projectionField = extractStringField(text, 'projection') + const projection = projectionField === null ? 'EPSG:3857' : projectionField + const tilePathTemplate = extractStringField(text, 'tilePathTemplate') + const tileFormatFromField = extractStringField(text, 'tileFormat') + const boundsValues = extractNumberArrayField(text, 'bounds') + + if (!Number.isFinite(minZoom) || !Number.isFinite(maxZoom) || !tilePathTemplate) { + throw new Error('meta.json 缺少必要字段') + } + + let tileFormat = tileFormatFromField || '' + if (!tileFormat) { + const extensionMatch = tilePathTemplate.match(/\.([A-Za-z0-9]+)$/) + tileFormat = extensionMatch ? extensionMatch[1].toLowerCase() : 'png' + } + + return { + tileSize, + minZoom: minZoom as number, + maxZoom: maxZoom as number, + projection, + tileFormat, + tilePathTemplate, + bounds: boundsValues && boundsValues.length >= 4 + ? [boundsValues[0], boundsValues[1], boundsValues[2], boundsValues[3]] + : null, + } +} + +function getBoundsCorners( + bounds: [number, number, number, number], + projection: string, +): { northWest: LonLatPoint; southEast: LonLatPoint; center: LonLatPoint } { + if (projection === 'EPSG:3857') { + const minX = bounds[0] + const minY = bounds[1] + const maxX = bounds[2] + const maxY = bounds[3] + return { + northWest: webMercatorToLonLat({ x: minX, y: maxY }), + southEast: webMercatorToLonLat({ x: maxX, y: minY }), + center: webMercatorToLonLat({ x: (minX + maxX) / 2, y: (minY + maxY) / 2 }), + } + } + + if (projection === 'EPSG:4326') { + const minLon = bounds[0] + const minLat = bounds[1] + const maxLon = bounds[2] + const maxLat = bounds[3] + return { + northWest: { lon: minLon, lat: maxLat }, + southEast: { lon: maxLon, lat: minLat }, + center: { lon: (minLon + maxLon) / 2, lat: (minLat + maxLat) / 2 }, + } + } + + throw new Error(`暂不支持的投影: ${projection}`) +} + +function buildTileBoundsByZoom( + bounds: [number, number, number, number] | null, + projection: string, + minZoom: number, + maxZoom: number, +): Record { + const boundsByZoom: Record = {} + if (!bounds) { + return boundsByZoom + } + + const corners = getBoundsCorners(bounds, projection) + for (let zoom = minZoom; zoom <= maxZoom; zoom += 1) { + const northWestWorld = lonLatToWorldTile(corners.northWest, zoom) + const southEastWorld = lonLatToWorldTile(corners.southEast, zoom) + const minX = Math.floor(Math.min(northWestWorld.x, southEastWorld.x)) + const maxX = Math.ceil(Math.max(northWestWorld.x, southEastWorld.x)) - 1 + const minY = Math.floor(Math.min(northWestWorld.y, southEastWorld.y)) + const maxY = Math.ceil(Math.max(northWestWorld.y, southEastWorld.y)) - 1 + + boundsByZoom[zoom] = { + minX, + maxX, + minY, + maxY, + } + } + + return boundsByZoom +} + +function getProjectionModeText(projection: string): string { + return `${projection} -> XYZ Tile -> Camera -> Screen` +} + +export function isTileWithinBounds( + tileBoundsByZoom: Record | null | undefined, + zoom: number, + x: number, + y: number, +): boolean { + if (!tileBoundsByZoom) { + return true + } + + const bounds = tileBoundsByZoom[zoom] + if (!bounds) { + return true + } + + return x >= bounds.minX && x <= bounds.maxX && y >= bounds.minY && y <= bounds.maxY +} + +export async function loadRemoteMapConfig(gameConfigUrl: string): Promise { + const gameConfigText = await requestText(gameConfigUrl) + const gameConfig = parseGameConfig(gameConfigText, gameConfigUrl) + const mapMetaUrl = resolveUrl(gameConfigUrl, gameConfig.mapMeta) + const mapRootUrl = resolveUrl(gameConfigUrl, gameConfig.mapRoot) + const mapMetaText = await requestText(mapMetaUrl) + const mapMeta = parseMapMeta(mapMetaText) + + const defaultZoom = clamp(17, mapMeta.minZoom, mapMeta.maxZoom) + const boundsCorners = mapMeta.bounds ? getBoundsCorners(mapMeta.bounds, mapMeta.projection) : null + const centerWorldTile = boundsCorners + ? lonLatToWorldTile(boundsCorners.center, defaultZoom) + : { x: 0, y: 0 } + + return { + tileSource: resolveUrl(mapRootUrl, mapMeta.tilePathTemplate), + minZoom: mapMeta.minZoom, + maxZoom: mapMeta.maxZoom, + defaultZoom, + initialCenterTileX: Math.round(centerWorldTile.x), + initialCenterTileY: Math.round(centerWorldTile.y), + projection: mapMeta.projection, + projectionModeText: getProjectionModeText(mapMeta.projection), + magneticDeclinationDeg: gameConfig.declinationDeg, + magneticDeclinationText: formatDeclinationText(gameConfig.declinationDeg), + tileFormat: mapMeta.tileFormat, + tileSize: mapMeta.tileSize, + bounds: mapMeta.bounds, + tileBoundsByZoom: buildTileBoundsByZoom(mapMeta.bounds, mapMeta.projection, mapMeta.minZoom, mapMeta.maxZoom), + mapMetaUrl, + mapRootUrl, + } +} + diff --git a/project.config.json b/project.config.json index 9496dbd..bbcb439 100644 --- a/project.config.json +++ b/project.config.json @@ -46,5 +46,5 @@ "ignore": [], "include": [] }, - "appid": "wx9d42aa29805ded5d" + "appid": "wx9cca5c5a219a4f9c" } \ No newline at end of file