import { Pos } from "@/utils/math.ts"; import { v4 as uuid } from "uuid"; /** * 四舍五入 * @param num * @param b 保留位数 * @returns */ export const round = (num: number, b: number = 2) => { const scale = Math.pow(10, b); return Math.round(num * scale) / scale; }; /** * 范围取余 * @param num * @param mod */ export const rangMod = (num: number, mod: number) => ((num % mod) + mod) % mod; /** * 有偏差的indexOf * @param arr * @param warp * @param val * @returns */ export const warpIndexOf = (arr: number[], warp: number, val: number) => arr.findIndex((num) => val >= num - warp && val <= num + warp); /** * 多个函数合并成一个函数 * @param fns * @returns */ export const mergeFuns = (...fns: (() => void)[] | (() => void)[][]) => { return () => { fns.forEach((fn) => { if (Array.isArray(fn)) { fn.forEach((f) => f()); } else { fn(); } }); }; }; export const copy = (data: T): T => JSON.parse(JSON.stringify(data)); /** * 获取数据类型 * @param value * @returns */ export const toRawType = (value: unknown): string => Object.prototype.toString.call(value).slice(8, -1); // 是否修改 const _inRevise = (raw1: any, raw2: any, readly: Set<[any, any]>): boolean => { if (raw1 === raw2) return false; const rawType1 = toRawType(raw1); const rawType2 = toRawType(raw2); if (rawType1 !== rawType2) { return true; } else if ( rawType1 === "String" || rawType1 === "Number" || rawType1 === "Boolean" ) { if (rawType1 === "Number" && isNaN(raw1) && isNaN(raw2)) { return false; } else { return raw1 !== raw2; } } const rawsArray = Array.from(readly.values()); for (const raws of rawsArray) { if (raws.includes(raw1) && raws.includes(raw2)) { return false; } } readly.add([raw1, raw2]); if (rawType1 === "Array") { return ( raw1.length !== raw2.length || raw1.some((item1: any, i: number) => _inRevise(item1, raw2[i], readly)) ); } else if (rawType1 === "Object") { const rawKeys1 = Object.keys(raw1).sort(); const rawKeys2 = Object.keys(raw2).sort(); return ( _inRevise(rawKeys1, rawKeys2, readly) || rawKeys1.some((key) => _inRevise(raw1[key], raw2[key], readly)) ); } else if (rawType1 === "Map") { const rawKeys1 = Array.from(raw1.keys()).sort(); const rawKeys2 = Array.from(raw2.keys()).sort(); return ( _inRevise(rawKeys1, rawKeys2, readly) || rawKeys1.some((key) => _inRevise(raw1.get(key), raw2.get(key), readly)) ); } else if (rawType1 === "Set") { return inRevise(Array.from(raw1.values()), Array.from(raw2.values())); } else { return raw1 !== raw2; } }; /** * 查看数据是否被修改 * @param raw1 * @param raw2 * @returns */ export const inRevise = (raw1: any, raw2: any) => _inRevise(raw1, raw2, new Set()); // 防抖 export const debounce = any>( fn: T, delay: number = 160 ) => { let timeout: any; return function (...args: Parameters) { clearTimeout(timeout); timeout = setTimeout(() => { fn.apply(null, args); }, delay); }; }; // 防抖 export const frameEebounce = any>(fn: T) => { let count = 0; return function (...args: Parameters) { let current = ++count; requestAnimationFrame(() => { if (current === count) { fn.apply(null, args); } }); }; }; /** * 获取数据变化 * @param newIds * @param oldIds * @returns */ export const getChangePart = (newIds: T[], oldIds: T[] = []) => { const addPort = newIds.filter((newId) => !oldIds.includes(newId)); const delPort = oldIds.filter((oldId) => !newIds.includes(oldId)); const holdPort = oldIds.filter((oldId) => newIds.includes(oldId)); return { addPort, delPort, holdPort }; }; export const flatPositions = (positions: Pos[]) => positions.flatMap((p) => [p.x, p.y]); export const flatToPositions = (coords: number[]) => { const positions: Pos[] = []; for (let i = 0; i < coords.length; i += 2) { positions.push({ x: coords[i], y: coords[i + 1], }); } return positions; }; export const onlyId = () => uuid(); export const startAnimation = (update: () => void, dur = -1) => { let isStop = false; const animation = () => { requestAnimationFrame(() => { if (!isStop) { update(); animation(); } }); }; animation(); let timeout: any; if (dur >= 0) { setTimeout(() => (isStop = true), dur); } return () => { clearTimeout(timeout); isStop = true; }; }; export const arrayInsert = ( array: T[], item: T, canInsert: (eItem: T, insertItem: T) => boolean ) => { let i = 0; for (i = 0; i < array.length; i++) { if (canInsert(array[i], item)) { break; } } array.splice(i, 0, item); return array; }; export const asyncTimeout = (time: number) => { let timeout: any; let reject: any; const promise = new Promise((resolve, r) => { timeout = setTimeout(resolve, time); reject = r; }) as Promise & { stop: () => void }; promise.stop = () => { clearTimeout(timeout); reject("取消"); }; return promise; }; export const getResizeCorsur = (level = true, r = 0) => { r = rangMod(r, 360); // 上下 "./icons/m_zoom_v.png" if (level) { if ((r > 0 && r < 20) || (r > 160 && r <= 200)) { return "./icons/m_zoom_h.png"; } if ((r >= 20 && r <= 70) || (r >= 200 && r <= 250)) { return "./icons/m_zoom.png"; } else if ((r > 70 && r < 110) || (r > 250 && r < 290)) { return "./icons/m_zoom_v.png"; } else if ((r >= 110 && r <= 160) || (r >= 290 && r <= 340)) { return "./icons/m_zoom_r.png"; } else { return "./icons/m_zoom_h.png"; } } else { if ((r > 0 && r < 20) || (r > 160 && r <= 200)) { return "./icons/m_zoom_v.png"; } if ((r >= 20 && r <= 70) || (r >= 200 && r <= 250)) { return "./icons/m_zoom_r.png"; } else if ((r > 70 && r < 110) || (r > 250 && r < 290)) { return "./icons/m_zoom_h.png"; } else if ((r >= 110 && r <= 160) || (r >= 290 && r <= 340)) { return "./icons/m_zoom.png"; } else { return "./icons/m_zoom_v.png"; } } }; export const diffArrayChange = >( newItems: T, oldItems: T ) => { const addedItems = [] as unknown as T; const deletedItems = [] as unknown as T; for (const item of newItems) { if (!oldItems.includes(item)) { addedItems.push(item); } } for (const item of oldItems) { if (!newItems.includes(item)) { deletedItems.push(item); } } return { added: addedItems, deleted: deletedItems, }; }; const DMSRG = /(\d+(?:\.\d+)?)°(?:(\d+(?:\.\d+)?)['|′])?(?:(\d+(?:\.\d+)?)["|″])?$/; export const dmsCheck = (dms: string) => { const r = DMSRG.exec(dms); return r && Number(r[2] || 0) < 60 && (Number(r[3]) || 0) < 60; }; // 度分秒转经纬度 export const toDigital = (dms: string) => { const r = DMSRG.exec(dms); if (r) { return round( Number(r[1]) + Number(r[2] || 0) / 60 + (Number(r[3]) || 0) / 3600, 12 ); } }; export const analysisGPS = (temp: string) => { const getGPS = (t: any[]) => { t = t.map((d) => (dmsCheck(d) ? toDigital(d) : Number(d))); if (t.length > 1 && t.every((v) => !Number.isNaN(v))) { return { x: t[0], y: t[1], } as Pos; } }; function isValidWGS84(lon: number, lat: number) { const isValidLon = lon >= -180 && lon <= 180; const isValidLat = lat >= -90 && lat <= 90; return isValidLon && isValidLat; } const splitChars = [",", " ", ","]; for (const splitChar of splitChars) { const gps = getGPS(temp.split(splitChar)); if (gps && isValidWGS84(gps.x, gps.y)) { return gps; } } }; export const tempStrFill = (tempStr: string, fill: Record) => { const regex = /\{(.*?)\}/g; let str = ""; let ndx = 0; let matches; while ((matches = regex.exec(tempStr)) !== null) { if (!(matches[1] in fill)) continue; str += tempStr.substring(ndx, matches.index) + fill[matches[1]]; ndx = matches.index + matches[0].length; } str += tempStr.substring(ndx, tempStr.length); return str; }; export const genBound = () => { let seted = false; let minX = Number.MAX_VALUE, minY = Number.MAX_VALUE, maxX = Number.MIN_VALUE, maxY = Number.MIN_VALUE; return { update(points: Pos | Pos[]) { points = Array.isArray(points) ? points : [points]; points.forEach((pos) => { seted = true; minX = Math.min(pos.x, minX); minY = Math.min(pos.y, minY); maxX = Math.max(pos.x, maxX); maxY = Math.max(pos.y, maxY); }); }, get() { if (!seted) return null; return { x: minX, y: minY, width: maxX - minX, height: maxY - minY, maxX, maxY, center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2, }, }; }, }; }; export const validNum = (num: any) => typeof num === "number" && !Number.isNaN(num); // 检测字体是否可用(返回第一个支持的字体) export function getSupportedFont(fonts: string[]) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d")!; const defaultFont = ctx.font; for (const font of fonts) { ctx.font = `12px ${font}`; if (ctx.font.includes(font)) { ctx.font = defaultFont; // 恢复默认 return font; } } ctx.font = defaultFont; return fonts[fonts.length - 1]; // 返回最后一个回退字体 } export const repeatedlyOnly = any>(fn: T) => { let empty = uuid(); let prevResult: any = empty; const pack = (...args: any[]) => { if (prevResult === empty) { const result = fn(...args); if (result instanceof Promise) { prevResult = result; return result.then((r) => { prevResult = empty; return r; }); } else { return result; } } else { return prevResult; } }; return pack as T; }; export const genCache = ( getKey: (args: T) => string | number, calcResult: (args: T) => R, delay: number | null = null ) => { const cache: Record = {}; return (args: T): R => { const key = getKey(args); if (key in cache) { return cache[key]; } const result = (cache[key] = calcResult(args)); if (delay !== null) { setTimeout(() => { delete cache[key]; }, delay); } return result; }; }; export const hoverManage = ( hoverHandler: (data: T) => (data: K) => void ) => { let enter = false; let timeout: any; let leave: undefined | ((data: K) => void); const enterHandler = (data: T) => { clearTimeout(timeout); if (!enter) { enter = true; leave = hoverHandler(data); } }; const immediatelyLeave = (data: K) => { enter = false; leave && leave(data); }; const leaveHandler = (data: K) => { timeout = setTimeout(() => { immediatelyLeave(data); }, 160); }; return { enterHandler, leaveHandler, immediatelyLeave, }; }; export const trackFlag = (trackCallback?: (flag: number) => void) => { let flag = 0; const callback = () => { flag-- trackCallback && trackCallback(flag) } return { get flag() { return flag }, track(fn: () => T): T { flag++; const result = fn(); if (result instanceof Promise) { return result.then((data) => { console.log('callback') callback() return data }) as T; } else { callback(); return result } }, }; };