You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/manager/ProvinceLocator.ts

205 lines
7.5 KiB

import { assetManager, JsonAsset, sys } from 'cc';
import { MiniGamePf } from '../../mx/tools/MiniGameHostInfo';
import { GBundle } from '../game/ConfigRes';
type ChinaProvincePack = {
/** 坐标放大倍数(lng/lat * s 后为整数) */
s: number;
p: Array<{
n: string;
/** [minLng, minLat, maxLng, maxLat],已乘 s */
b: number[];
/** 外环,扁平 [lng,lat,lng,lat...],已乘 s */
r: number[][];
}>;
};
type CachePayload = { n: string };
const CACHE_KEY = 'province_locator_v1';
const DATA_PATH = 'config/china_provinces';
const LOCATE_TIMEOUT_MS = 10000;
/**
* 一次定位 + 本地省界判断。命中后写入本地缓存,之后不再调定位。
*/
export class ProvinceLocator {
private static _cacheLoaded = false;
private static _cached: string | null = null;
private static _inflight: Promise<string> | null = null;
private static _pack: ChinaProvincePack | null = null;
/** 空缓存本进程内允许静默重试一次 */
private static _emptyRetryDone = false;
/**
* 返回短省名(广东 / 北京);未授权、失败或未命中返回空串。
* @param options.allowEmptyCacheRetry 上次失败空缓存时,本进程内是否允许再定位一次
* @param options.force 清掉空失败缓存后强制重新定位(授权成功后用)
*/
static getProvince(options?: { allowEmptyCacheRetry?: boolean; force?: boolean }): Promise<string> {
this._readCache();
if (options?.force && this._cached === '') {
this._cached = null;
this._emptyRetryDone = false;
}
if (this._cached != null && this._cached !== '') return Promise.resolve(this._cached);
if (this._cached === '') {
if (!options?.allowEmptyCacheRetry || this._emptyRetryDone) {
return Promise.resolve('');
}
this._emptyRetryDone = true;
}
if (this._inflight) return this._inflight;
this._inflight = this._resolve().finally(() => {
this._inflight = null;
});
return this._inflight;
}
private static async _resolve(): Promise<string> {
const loc = await this._getLatLng();
if (!loc) return '';
if ('denied' in loc) {
this._writeCache('');
return '';
}
const pack = await this._loadPack();
if (!pack) return '';
const name = this._hitProvince(loc.lng, loc.lat, pack);
this._writeCache(name);
return name;
}
private static _readCache(): void {
if (this._cacheLoaded) return;
this._cacheLoaded = true;
try {
const raw = sys.localStorage.getItem(CACHE_KEY);
if (!raw) return;
const o = JSON.parse(raw) as CachePayload;
if (typeof o?.n === 'string') this._cached = o.n;
} catch {
/* ignore */
}
}
private static _writeCache(name: string): void {
this._cached = name;
try {
const payload: CachePayload = { n: name };
sys.localStorage.setItem(CACHE_KEY, JSON.stringify(payload));
} catch {
/* ignore */
}
}
private static _loadPack(): Promise<ChinaProvincePack | null> {
if (this._pack) return Promise.resolve(this._pack);
return new Promise((resolve) => {
const take = (bundle: ReturnType<typeof assetManager.getBundle>) => {
if (!bundle) {
resolve(null);
return;
}
const cached = bundle.get(DATA_PATH, JsonAsset);
if (cached?.json) {
this._pack = cached.json as ChinaProvincePack;
resolve(this._pack);
return;
}
bundle.load(DATA_PATH, JsonAsset, (err, asset) => {
if (err || !asset?.json) {
resolve(null);
return;
}
this._pack = asset.json as ChinaProvincePack;
resolve(this._pack);
});
};
const existed = assetManager.getBundle(GBundle.First);
if (existed) {
take(existed);
return;
}
assetManager.loadBundle(GBundle.First, (err, bundle) => take(err ? null : bundle));
});
}
private static _getLatLng(): Promise<{ lat: number; lng: number } | { denied: true } | null> {
const host = MiniGamePf.api();
const wx = (window as Record<string, unknown>)['wx'] as MiniLocationApi | undefined;
const api = (host?.getLocation ? host : wx) as MiniLocationApi | undefined;
if (!api?.getLocation) return Promise.resolve(null);
return new Promise((resolve) => {
let done = false;
const finish = (v: { lat: number; lng: number } | { denied: true } | null) => {
if (done) return;
done = true;
resolve(v);
};
const timer = setTimeout(() => finish(null), LOCATE_TIMEOUT_MS);
const opts: Parameters<MiniLocationApi['getLocation']>[0] = {
success: (res) => {
clearTimeout(timer);
const lat = Number(res?.latitude);
const lng = Number(res?.longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) finish(null);
else finish({ lat, lng });
},
fail: (res) => {
clearTimeout(timer);
const msg = String(res?.errMsg ?? '');
if (/auth deny|authorize|permission|privacy/i.test(msg)) finish({ denied: true });
else finish(null);
},
};
// B 站无 type;抖音/微信传 gcj02 对齐国内省界
if (!MiniGamePf.isBl()) opts.type = 'gcj02';
api.getLocation(opts);
});
}
private static _hitProvince(lng: number, lat: number, pack: ChinaProvincePack): string {
const s = pack.s || 10000;
const x = lng * s;
const y = lat * s;
const list = pack.p;
for (let i = 0; i < list.length; i++) {
const item = list[i];
const b = item.b;
if (x < b[0] || y < b[1] || x > b[2] || y > b[3]) continue;
const rings = item.r;
for (let k = 0; k < rings.length; k++) {
if (this._pointInRing(x, y, rings[k])) return item.n;
}
}
return '';
}
/** 射线法:点是否在多边形内(ring 为已乘 s 的扁平坐标) */
private static _pointInRing(x: number, y: number, ring: number[]): boolean {
const n = (ring.length / 2) | 0;
let inside = false;
let j = n - 1;
for (let i = 0; i < n; i++) {
const xi = ring[i * 2];
const yi = ring[i * 2 + 1];
const xj = ring[j * 2];
const yj = ring[j * 2 + 1];
const dy = yj - yi;
if (dy !== 0 && (yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / dy + xi) {
inside = !inside;
}
j = i;
}
return inside;
}
}
type MiniLocationApi = {
getLocation: (opts: {
type?: string;
success?: (res: { latitude?: number; longitude?: number }) => void;
fail?: (res: { errMsg?: string }) => void;
}) => void;
};