|
|
@@ -1,38 +1,76 @@
|
|
|
-import { useGlobSetting } from '@/core/hooks/setting';
|
|
|
+import { isDesktopDialogAvailable, invokeIpc } from '@/core/utils/desktop/ipcRenderer';
|
|
|
|
|
|
const OFFLINE_MAP_DIR_KEY = 'OFFLINE_MAP_DIR__';
|
|
|
-const OFFLINE_MAP_SERVER_DIR_KEY = 'OFFLINE_MAP_SERVER_DIR__';
|
|
|
export const OFFLINE_MAP_DIR_CHANGED_EVENT = 'offline-map-dir-changed';
|
|
|
const LOCAL_MAP_PROTOCOL = 'localmap://';
|
|
|
const PMTILES_PROTOCOL = 'pmtiles://';
|
|
|
|
|
|
-/** 后端可达前缀(与 ai/utils/constants.ts 的 resolveApiUrl 同一套约定:apiUrl + urlPrefix + adminPath) */
|
|
|
-const buildApiUrl = (path: string) => {
|
|
|
- const { apiUrl, urlPrefix, adminPath } = useGlobSetting();
|
|
|
- const prefix = `${apiUrl || ''}${urlPrefix || ''}${adminPath || ''}`.replace(/\/+$/, '');
|
|
|
- return `${prefix}${path.startsWith('/') ? path : `/${path}`}`;
|
|
|
+/** 前端静态资源目录里的兜底瓦片地址(public/resource/map/china.pmtiles,未配置本机目录时使用) */
|
|
|
+export const getBuiltinPmtilesUrl = () =>
|
|
|
+ new URL('resource/map/china.pmtiles', new URL(import.meta.env.BASE_URL || './', window.location.href)).toString();
|
|
|
+
|
|
|
+/** 本机离线地图文件路径(配置目录 + 文件名);未配置目录返回空串 */
|
|
|
+export function getOfflineMapFilePath(fileName: string) {
|
|
|
+ const dir = getOfflineMapDir();
|
|
|
+ if (!dir || !fileName) return '';
|
|
|
+ return `${dir.replace(/[\/]+$/, '')}/${fileName}`;
|
|
|
+}
|
|
|
+
|
|
|
+/** 瓦片文件(china.pmtiles)的本机绝对路径 */
|
|
|
+export const getOfflineMapPmtilesPath = () => getOfflineMapFilePath('china.pmtiles');
|
|
|
+
|
|
|
+/** 自定义样式文件的本机绝对路径 */
|
|
|
+export const getOfflineMapStylePath = () => getOfflineMapFilePath('behavior-spacetime-style.json');
|
|
|
+
|
|
|
+const base64ToArrayBuffer = (base64: string) => {
|
|
|
+ const binary = window.atob(base64);
|
|
|
+ const bytes = new Uint8Array(binary.length);
|
|
|
+ for (let index = 0; index < binary.length; index += 1) {
|
|
|
+ bytes[index] = binary.charCodeAt(index);
|
|
|
+ }
|
|
|
+ return bytes.buffer;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 离线地图瓦片(china.pmtiles)的 Web 访问地址。
|
|
|
+ * 按本机路径分块读 PMTiles 的 Source 实现(恢复改造前的本机加载方式)。
|
|
|
*
|
|
|
- * <p>Web 化改造:瓦片放在**服务端**离线地图目录(系统配置 mapLocal)里,
|
|
|
- * 由后端 `GET /sys/map/pmtiles` 支持 HTTP Range 按块返回,替代原 Electron 的 ipc 读取。
|
|
|
+ * <p>PMTiles 只要求实现 {@code getKey()} 与 {@code getBytes(offset, length)};地图样式里的
|
|
|
+ * source url 写成 {@code pmtiles://<本机绝对路径>},数据由主进程 IPC(osCtl.readBinaryFileRange)
|
|
|
+ * 按需从磁盘取块 —— 不经过服务端,也就不受「后端目录/后端 Range 实现」影响。
|
|
|
*/
|
|
|
-export const getOfflineMapPmtilesUrl = () => buildApiUrl('/sys/map/pmtiles');
|
|
|
+export class ElectronPmtilesSource {
|
|
|
+ filePath: string;
|
|
|
|
|
|
-/** 离线地图自定义样式(配置目录里的 behavior-spacetime-style.json)的访问地址 */
|
|
|
-export const getOfflineMapStyleUrl = () => buildApiUrl('/sys/map/style');
|
|
|
+ constructor(filePath: string) {
|
|
|
+ this.filePath = String(filePath || '').trim();
|
|
|
+ }
|
|
|
|
|
|
-/** 前端静态资源目录里的兜底瓦片地址(public/resource/map/china.pmtiles,未配置服务端目录时使用) */
|
|
|
-export const getBuiltinPmtilesUrl = () =>
|
|
|
- new URL('resource/map/china.pmtiles', new URL(import.meta.env.BASE_URL || './', window.location.href)).toString();
|
|
|
+ getKey() {
|
|
|
+ return this.filePath;
|
|
|
+ }
|
|
|
+
|
|
|
+ async getBytes(offset: number, length: number) {
|
|
|
+ if (!this.filePath || !length || !isDesktopDialogAvailable()) {
|
|
|
+ return { data: new ArrayBuffer(0) };
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const result = await invokeIpc<
|
|
|
+ { success: true; data: { data: string; size: number } } | { success: false; message: string }
|
|
|
+ >('readBinaryFileRange', { filePath: this.filePath, offset, length });
|
|
|
+ if (!result?.success) return { data: new ArrayBuffer(0) };
|
|
|
+ const base64 = String(result.data?.data || '');
|
|
|
+ if (!base64 || !Number(result.data?.size || 0)) {
|
|
|
+ return { data: new ArrayBuffer(0) };
|
|
|
+ }
|
|
|
+ return { data: base64ToArrayBuffer(base64) };
|
|
|
+ } catch {
|
|
|
+ return { data: new ArrayBuffer(0) };
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
- * 通过 HTTP Range 读取 PMTiles 的 Source 实现(替代原 ElectronPmtilesSource)。
|
|
|
- *
|
|
|
- * <p>PMTiles 只要求实现 {@code getKey()} 与 {@code getBytes(offset, length)};
|
|
|
- * 地图样式里的 source url 写成 {@code pmtiles://<本类的 url>} 即会按需分块拉取。
|
|
|
+ * 通过 HTTP Range 读取 PMTiles 的 Source 实现(仅用于前端静态资源里的兜底瓦片)。
|
|
|
*/
|
|
|
export class HttpPmtilesSource {
|
|
|
url: string;
|
|
|
@@ -63,9 +101,46 @@ export class HttpPmtilesSource {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** 注册离线地图瓦片源:配置了服务端目录用后端 Range 接口,否则退回前端静态资源 */
|
|
|
-export const resolveOfflinePmtilesUrls = () =>
|
|
|
- [getOfflineMapDir() ? getOfflineMapPmtilesUrl() : '', getBuiltinPmtilesUrl()].filter(Boolean);
|
|
|
+/** 本机绝对路径(盘符 / UNC / POSIX)才走 Electron 读盘,其余(http(s)、相对 URL)走 fetch */
|
|
|
+export const isLocalFilePath = (target: string) => {
|
|
|
+ const text = String(target || '').trim();
|
|
|
+ return /^[a-zA-Z]:[\\/]/.test(text) || text.startsWith('\\\\') || text.startsWith('/');
|
|
|
+};
|
|
|
+
|
|
|
+/** 本机绝对路径走 Electron 读盘,其余(内置静态资源 URL)走 HTTP Range */
|
|
|
+export const createPmtilesSource = (target: string) =>
|
|
|
+ isLocalFilePath(target) ? new ElectronPmtilesSource(target) : new HttpPmtilesSource(target);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 离线地图瓦片来源列表:配了本机目录就用本机文件(key 为本机绝对路径),否则退回内置静态资源。
|
|
|
+ * key 与样式里 source url 的 {@code pmtiles://<key>} 必须一致,pmtiles Protocol 才认。
|
|
|
+ */
|
|
|
+export const resolveOfflinePmtilesSources = (): Array<{
|
|
|
+ key: string;
|
|
|
+ source: ElectronPmtilesSource | HttpPmtilesSource;
|
|
|
+}> => {
|
|
|
+ const sources: Array<{ key: string; source: ElectronPmtilesSource | HttpPmtilesSource }> = [];
|
|
|
+ const localPath = getOfflineMapPmtilesPath();
|
|
|
+ if (localPath) sources.push({ key: localPath, source: new ElectronPmtilesSource(localPath) });
|
|
|
+ const builtinUrl = getBuiltinPmtilesUrl();
|
|
|
+ sources.push({ key: builtinUrl, source: new HttpPmtilesSource(builtinUrl) });
|
|
|
+ return sources;
|
|
|
+};
|
|
|
+
|
|
|
+/** 读本机离线地图样式 JSON 文本;未配置目录/非桌面端/读失败返回空串(调用方回退内置样式) */
|
|
|
+export const readOfflineMapStyleText = async (): Promise<string> => {
|
|
|
+ const filePath = getOfflineMapStylePath();
|
|
|
+ if (!filePath || !isDesktopDialogAvailable()) return '';
|
|
|
+ try {
|
|
|
+ const result = await invokeIpc<{ success: true; data: { text: string } } | { success: false; message: string }>(
|
|
|
+ 'readTextFile',
|
|
|
+ { filePath, encoding: 'utf8' },
|
|
|
+ );
|
|
|
+ return result?.success ? String(result.data?.text || '') : '';
|
|
|
+ } catch {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
/** 本地路径是否可用(绝对路径 / UNC / file URL):设置页保存前用它拦掉明显无效的输入 */
|
|
|
export function isValidOfflineMapPath(value: string) {
|
|
|
@@ -87,10 +162,9 @@ function normalizePath(value: string) {
|
|
|
return text.replace(/[\\/]+$/, '');
|
|
|
}
|
|
|
|
|
|
+/** 生效的离线地图目录:本机设置(Electron 保存的那份)同步过来的缓存值 */
|
|
|
export function getOfflineMapDir() {
|
|
|
try {
|
|
|
- const serverDir = normalizePath(window.localStorage.getItem(OFFLINE_MAP_SERVER_DIR_KEY) || '');
|
|
|
- if (serverDir) return serverDir;
|
|
|
return normalizePath(window.localStorage.getItem(OFFLINE_MAP_DIR_KEY) || '');
|
|
|
} catch {
|
|
|
return '';
|
|
|
@@ -105,14 +179,6 @@ export function getManualOfflineMapDir() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export function getServerOfflineMapDir() {
|
|
|
- try {
|
|
|
- return normalizePath(window.localStorage.getItem(OFFLINE_MAP_SERVER_DIR_KEY) || '');
|
|
|
- } catch {
|
|
|
- return '';
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
export function setOfflineMapDir(path: string) {
|
|
|
const normalized = normalizePath(path);
|
|
|
try {
|
|
|
@@ -130,19 +196,6 @@ export function clearOfflineMapDir() {
|
|
|
setOfflineMapDir('');
|
|
|
}
|
|
|
|
|
|
-export function setServerOfflineMapDir(path: string) {
|
|
|
- const normalized = normalizePath(path);
|
|
|
- try {
|
|
|
- if (!normalized) {
|
|
|
- window.localStorage.removeItem(OFFLINE_MAP_SERVER_DIR_KEY);
|
|
|
- window.dispatchEvent(new CustomEvent(OFFLINE_MAP_DIR_CHANGED_EVENT, { detail: getOfflineMapDir() }));
|
|
|
- return;
|
|
|
- }
|
|
|
- window.localStorage.setItem(OFFLINE_MAP_SERVER_DIR_KEY, normalized);
|
|
|
- window.dispatchEvent(new CustomEvent(OFFLINE_MAP_DIR_CHANGED_EVENT, { detail: normalized }));
|
|
|
- } catch {}
|
|
|
-}
|
|
|
-
|
|
|
export function buildFileUrl(filePath: string) {
|
|
|
const normalized = normalizePath(filePath).replace(/\\/g, '/');
|
|
|
if (!normalized) return '';
|