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.
205 lines
7.5 KiB
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;
|
|
};
|
|
|