// ==UserScript== // @name 全局强制虚拟定位(双重 Hook + 微抖动 + 状态回显) // @namespace http://tampermonkey.net/ // @version 3.0.0 // @description Hook getCurrentPosition + watchPosition,强制返回虚拟坐标;中国视图地图选点 + 状态栏回显 + 星号收藏夹 // @author you // @match *://*/* // @run-at document-start // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_getResourceText // @grant GM_addStyle // @require https://unpkg.com/leaflet@1.9.4/dist/leaflet.js // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @resource leafletCSS https://unpkg.com/leaflet@1.9.4/dist/leaflet.css // ==/UserScript== (function () { 'use strict'; // ============================================================ // 第 0 部分:常量 & 存储 // ============================================================ var DEF_LAT = 35.0, DEF_LNG = 105.0; // 默认中国中心 var ACC_BASE = 8; // 精度基准值(米) function getFakeLat() { return GM_getValue('vg_lat', null); } function getFakeLng() { return GM_getValue('vg_lng', null); } function getFakeTs() { return GM_getValue('vg_ts', 0); } var favs = GM_getValue('vg_favs', []); function isVirtual() { return getFakeLat() !== null && getFakeLng() !== null; } // ============================================================ // === 微抖动算法(Anti-Fingerprinting,严格按指定公式) === // ============================================================ function applyJitter(baseLat, baseLng) { var METER_TO_DEG = 1 / 111000; // 1 米 ≈ 0.000009009 度 var MAX_JITTER = 0.2 * METER_TO_DEG; // 半径 0.2 米(手持 GPS 漂移) var jitteredLat = baseLat + (Math.random() - 0.5) * 2 * MAX_JITTER; var jitteredLng = baseLng + (Math.random() - 0.5) * 2 * MAX_JITTER / Math.cos(baseLat * Math.PI / 180); return { lat: jitteredLat, lng: jitteredLng }; } // ============================================================ // === 状态栏 & Logging === // ============================================================ var LOG_STYLE = { init: 'color: cyan;', virtual: 'color: green;', real: 'color: yellow;', fav: 'color: gold;' }; function lg(tag, msg, style) { console.log('%c ' + tag + ' ' + msg, LOG_STYLE[style] || LOG_STYLE.init); } // ============================================================ // === getCurrentPosition Hook === // ============================================================ if (typeof Geolocation !== 'undefined' && Geolocation.prototype) { Geolocation.prototype.getCurrentPosition = function (success, error, options) { var baseLat = getFakeLat() || DEF_LAT; var baseLng = getFakeLng() || DEF_LNG; var j = applyJitter(baseLat, baseLng); var acc = ACC_BASE + (Math.random() * 4 - 2); // 6~10m 随机 lg('[GPS Return]', 'Virtual | Lat: ' + j.lat.toFixed(6) + ', Lng: ' + j.lng.toFixed(6) + ' (Acc: ' + acc.toFixed(1) + 'm)', 'virtual'); var pos = { coords: { latitude: j.lat, longitude: j.lng, accuracy: acc, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }; setTimeout(function () { try { success(pos); } catch (e) { console.error('[Virtual GPS] 回调异常', e); } }, 0); }; lg('[GPS Hook]', 'getCurrentPosition initialized', 'init'); } // ============================================================ // === watchPosition Hook === // ============================================================ if (typeof Geolocation !== 'undefined' && Geolocation.prototype) { var _watchIdCounter = 0; var _watchMap = {}; // id → 已返回的位置(保证同一 watcher 返回一致坐标) Geolocation.prototype.watchPosition = function (success, error, options) { var id = ++_watchIdCounter; var baseLat = getFakeLat() || DEF_LAT; var baseLng = getFakeLng() || DEF_LNG; var j = applyJitter(baseLat, baseLng); var acc = ACC_BASE + (Math.random() * 4 - 2); lg('[GPS Return]', 'Virtual (Watch #' + id + ') | Lat: ' + j.lat.toFixed(6) + ', Lng: ' + j.lng.toFixed(6) + ' (Acc: ' + acc.toFixed(1) + 'm)', 'virtual'); var pos = { coords: { latitude: j.lat, longitude: j.lng, accuracy: acc, altitude: null, altitudeAccuracy: null, heading: null, speed: null }, timestamp: Date.now() }; _watchMap[id] = pos; // 保存,模拟同一 watcher 的持续一致性 setTimeout(function () { try { success(pos); } catch (e) { console.error('[Virtual GPS] Watch 回调异常', e); } }, 0); return id; // 返回 watchId,供 clearWatch 使用 }; Geolocation.prototype.clearWatch = function (id) { if (_watchMap[id]) { delete _watchMap[id]; lg('[GPS Hook]', 'Watch #' + id + ' cleared', 'init'); } }; lg('[GPS Hook]', 'watchPosition initialized', 'init'); } // ============================================================ // === 样式注入 === // ============================================================ GM_addStyle(GM_getResourceText('leafletCSS')); GM_addStyle( // 状态栏(40px,常驻,地图顶部) '#vg_status{height:40px;min-height:40px;display:flex;align-items:center;justify-content:space-between;' + 'padding:0 16px;font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;font-weight:600;}' + '#vg_status.virtual{background:#4CAF50;color:#fff;}' + '#vg_status.real{background:#FF9800;color:#fff;}' + '#vg_status .src{background:rgba(255,255,255,.25);padding:2px 8px;border-radius:10px;font-size:11px;}' + // 模态框 & 布局 '#vg_overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.55);' + 'z-index:99999;display:flex;align-items:center;justify-content:center;}' + '#vg_modal{background:#fff;border-radius:12px;width:88vw;max-width:1100px;height:82vh;' + 'display:flex;flex-direction:column;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.35);}' + '#vg_head{padding:12px 16px;font:700 15px/1.4 "PingFang SC","Microsoft YaHei",sans-serif;' + 'color:#1f2937;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #e5e7eb;}' + '#vg_close{cursor:pointer;font-size:22px;color:#9ca3af;line-height:1;padding:0 4px;}' + '#vg_close:hover{color:#111827;}' + '#vg_body{flex:1;display:flex;min-height:0;}' + // 收藏夹侧边栏 '#vg_side{width:200px;min-width:200px;border-right:1px solid #e5e7eb;display:flex;flex-direction:column;' + 'font-family:"PingFang SC","Microsoft YaHei",sans-serif;}' + '#vg_side_t{padding:10px 12px;font-size:13px;font-weight:700;color:#374151;border-bottom:1px solid #f3f4f6;}' + '#vg_side_list{flex:1;overflow-y:auto;padding:4px 6px;}' + '.vg_fav{display:flex;justify-content:space-between;align-items:center;padding:8px 6px;' + 'border-bottom:1px solid #f3f4f6;cursor:pointer;border-radius:6px;}' + '.vg_fav:hover{background:#f9fafb;}' + '.vg_fav .nm{flex:1;font-size:13px;color:#1f2937;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}' + '.vg_fav .del{margin-left:6px;cursor:pointer;font-size:12px;opacity:.6;padding:2px;}' + '.vg_fav .del:hover{opacity:1;}' + '.vg_empty{color:#9ca3af;font-size:12px;padding:16px 6px;text-align:center;line-height:1.8;}' + // 地图 '#vg_map{flex:1;min-width:0;height:100%;}' + '#vg_tip{padding:8px;text-align:center;font-size:12px;color:#6b7280;border-top:1px solid #e5e7eb;' + 'font-family:"PingFang SC","Microsoft YaHei",sans-serif;}' + // 星号按钮 '#vg_star{margin-top:6px;background:#fff;border:1px solid #d1d5db;border-radius:8px;' + 'padding:5px 14px;font-size:13px;cursor:pointer;color:#6b7280;transition:.15s;}' + '#vg_star:hover{border-color:#f59e0b;color:#f59e0b;}' + '#vg_star.on{border-color:#f59e0b;color:#f59e0b;background:#fffbeb;}' + '.swal2-container{z-index:100000 !important;}' ); // ============================================================ // === 模态框 + 地图初始化 === // ============================================================ var vgMap = null, vgMarker = null; var selLat = 0, selLng = 0, selFav = null; delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png' }); function openModal() { if (document.getElementById('vg_overlay')) return; if (!document.body) { setTimeout(openModal, 100); return; } var overlay = document.createElement('div'); overlay.id = 'vg_overlay'; // ============================================================ // === 状态栏(地图顶部 40px 常驻,避免闪烁) === // ============================================================ var isVirt = isVirtual(); var statusHtml; if (isVirt) { var lat = getFakeLat(), lng = getFakeLng(), ts = getFakeTs(); var dateStr = ts ? new Date(ts).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '未知'; statusHtml = '