您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Replace teleport-like jumps on the neal.fun/internet-roadtrip minimap with portal markers
// ==UserScript== // @name Internet Roadtrip - Minimap Teleport Markers // @description Replace teleport-like jumps on the neal.fun/internet-roadtrip minimap with portal markers // @namespace me.netux.site/user-scripts/internet-roadtrip/minimap-teleport-markers // @version 0.1.3 // @author netux // @license MIT // @match https://neal.fun/internet-roadtrip/ // @icon https://neal.fun/favicons/internet-roadtrip.png // @grant GM_addStyle // @grant GM.setValue // @grant GM.getValue // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected] // ==/UserScript== /* globals IRF */ (async () => { const MOD_NAME = GM.info.script.name.replace('Internet Roadtrip -', '').trim(); const MOD_PREFIX = 'mtpm-'; const PORTAL_ICONS_SOURCE_NAME = `${MOD_PREFIX}portal-icons`; const PORTAL_ICONS_LAYER_NAME = `${MOD_PREFIX}portal-icons`; const TELEPORT_LINES_SOURCE_NAME = `${MOD_PREFIX}teleport-lines`; const TELEPORT_LINES_LAYER_NAME = `${MOD_PREFIX}teleport-lines`; const TELEPORT_DISTANCE_THRESHOLD_METERS = 5_000; //const PORTAL_ENTRANCE_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-entrance.svg'; const PORTAL_ENTRANCE_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-entrance.png'; const PORTAL_ENTRANCE_IMAGE_NAME = 'portal-entrance'; //const PORTAL_EXIT_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-exit.svg'; const PORTAL_EXIT_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-exit.png'; const PORTAL_EXIT_IMAGE_NAME = 'portal-exit'; const cssClass = (... names) => names.map((name) => MOD_PREFIX + name).join(' '); const cssProp = (name) => `--${MOD_PREFIX}${name}`; const RAD_TO_DEG = 180 / Math.PI; const DEG_TO_RAD = 1 / RAD_TO_DEG; const util = { // Yoinked from Chris. // Thanks Chris! haversineDistance(lat1, lon1, lat2, lon2) { const R = 6371e3; // Earth radius in meters const phi1 = lat1 * DEG_TO_RAD; const phi2 = lat2 * DEG_TO_RAD; const deltaPhi = (lat2 - lat1) * DEG_TO_RAD; const deltaLambda = (lon2 - lon1) * DEG_TO_RAD; const a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; }, // Yoinked from Jakub. Thanks Jakub! // Helper functions for working with tiles // Based on https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames gcsToTileCoordinates(x, y, z) { const n = 2**z const lon_deg = x / n * 360.0 - 180.0 const lat_rad = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) const lat_deg = lat_rad * 180.0 / Math.PI return [lat_deg, lon_deg] }, tileToGcsCoordinates([lat, lng], z) { const n = 2 ** z; const x = n * ((lng + 180) / 360); const latRad = lat / 180 * Math.PI; const y = n * (1 - (Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI)) / 2; return [Math.floor(x), Math.floor(y), Math.floor(z)]; } } const settings = { portalIcons: { layer: { show: true, opacity: 1, }, }, teleportLines: { layer: { show: true, opacity: 0.5 }, } }; for (const key in settings) { settings[key] = await GM.getValue(key, settings[key]); } async function saveSettings() { for (const key in settings) { await GM.setValue(key, settings[key]); } } const mapVDOM = await IRF.vdom.map; const map = mapVDOM.data.map; let isMapLoaded = map.loaded(); const waitForMapToLoad = new Promise((resolve) => { map.once('load', () => { isMapLoaded = true; resolve(); }); }); function updateUiFromSettings() { map.setLayoutProperty(PORTAL_ICONS_LAYER_NAME, 'visibility', settings.portalIcons.layer.show ? 'visible' : 'none'); map.setPaintProperty(PORTAL_ICONS_LAYER_NAME, 'icon-opacity', settings.portalIcons.layer.opacity); map.setLayoutProperty(TELEPORT_LINES_LAYER_NAME, 'visibility', settings.teleportLines.layer.show ? 'visible' : 'none'); map.setPaintProperty(TELEPORT_LINES_LAYER_NAME, 'line-opacity', settings.teleportLines.layer.opacity); } const portalIconsSourceGeojsonData = { type: 'FeatureCollection', features: [] }; const teleportLinesSourceGeojsonData = { type: 'Feature', geometry: { type: 'MultiLineString', coordinates: [] } }; waitForMapToLoad.then(() => { map.loadImage(PORTAL_ENTRANCE_IMAGE_URL) .then((image) => map.addImage(PORTAL_ENTRANCE_IMAGE_NAME, image.data)) .catch((error) => { console.error(`[${MOD_NAME}] Could not load portal entrance image:`, error) }); map.loadImage(PORTAL_EXIT_IMAGE_URL) .then((image) => map.addImage(PORTAL_EXIT_IMAGE_NAME, image.data)) .catch((error) => { console.error(`[${MOD_NAME}] Could not load portal exit image:`, error) }); map.addSource(PORTAL_ICONS_SOURCE_NAME, { type: 'geojson', data: portalIconsSourceGeojsonData }); map.addLayer({ id: PORTAL_ICONS_LAYER_NAME, source: PORTAL_ICONS_SOURCE_NAME, type: 'symbol', minzoom: 5, layout: { 'icon-image': ['get', 'icon'], 'icon-rotate': ['get', 'angle'], 'icon-size': [ 'interpolate', ['linear'], ['zoom'], /* map zoom, icon-size */ 5, 0.02, 18, 0.1, ], 'icon-allow-overlap': true } }); map.addSource(TELEPORT_LINES_SOURCE_NAME, { type: 'geojson', data: teleportLinesSourceGeojsonData }); map.addLayer({ id: TELEPORT_LINES_LAYER_NAME, source: TELEPORT_LINES_SOURCE_NAME, type: 'line', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': 'orange', 'line-width': 3 } }); updateUiFromSettings(); }); //map.on('zoom', () => console.debug(map.getZoom())); const isTeleport = ([prevLat, prevLng], [thisLat, thisLng]) => util.haversineDistance(prevLat, prevLng, thisLat, thisLng) > TELEPORT_DISTANCE_THRESHOLD_METERS; function addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]) { // Add teleport line to its own layer teleportLinesSourceGeojsonData.geometry.coordinates.push([ [prevLng, prevLat], [thisLng, thisLat] ]); // Create portal icons const angle = Math.atan2(thisLat - prevLat, thisLng - prevLng) * RAD_TO_DEG; portalIconsSourceGeojsonData.features.push({ type: 'Feature', properties: { 'icon': PORTAL_ENTRANCE_IMAGE_NAME, 'angle': angle, 'portal-type': 'entrance' }, geometry: { type: 'Point', coordinates: [prevLng, prevLat] } }); portalIconsSourceGeojsonData.features.push({ type: 'Feature', properties: { 'icon': PORTAL_EXIT_IMAGE_NAME, 'angle': -angle % 360, 'portal-type': 'exit' }, geometry: { type: 'Point', coordinates: [thisLng, thisLat] } }); } function updateModLayerSources() { map.getSource(PORTAL_ICONS_SOURCE_NAME).setData(portalIconsSourceGeojsonData); map.getSource(TELEPORT_LINES_SOURCE_NAME).setData(teleportLinesSourceGeojsonData); } let lastOldRouteCoord = null; async function resolveTeleportsInOldRoute() { const oldRouteLayer = map.getLayer('old-route-layer'); oldRouteLayer.setPaintProperty('line-gradient', 'red'); map.moveLayer('old-route-layer', /* before: */ PORTAL_ICONS_LAYER_NAME); const oldRouteSource = map.getSource('old-route'); const oldRouteData = await oldRouteSource.getData(); const customOldRouteGeojsonData = { type: 'Feature', geometry: { type: 'MultiLineString', coordinates: [] } }; function newLineStringArray() { const lineStringArray = []; customOldRouteGeojsonData.geometry.coordinates.push(lineStringArray); return lineStringArray; }; let currentLineStringArray = newLineStringArray(); for (let i = 0; i < oldRouteData.geometry.coordinates.length; i++) { const currentCoords = oldRouteData.geometry.coordinates[i]; currentLineStringArray.push(currentCoords); if (i >= 1) { const [prevLng, prevLat] = oldRouteData.geometry.coordinates[i - 1]; const [thisLng, thisLat] = currentCoords; if (isTeleport([prevLat, prevLng], [thisLat, thisLng])) { // Remove teleport line from route, and // setup new line for the rest of the route. currentLineStringArray.pop(); currentLineStringArray = newLineStringArray(); currentLineStringArray.push(currentCoords); addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]); } } lastOldRouteCoord = currentCoords; } //console.debug(customOldRouteGeojsonData); //console.debug(portalIconsSourceGeojsonData); //console.debug(teleportLinesSourceGeojsonData); oldRouteSource.setData(customOldRouteGeojsonData); updateModLayerSources(); } async function patchRouteSourceAndLayer() { const routeSource = map.getSource('route'); const customRouteGeojsonData = { type: 'Feature', geometry: { type: 'MultiLineString', coordinates: [ [] ] } }; const ogSourceSetData = routeSource.setData; routeSource.setData = new Proxy(routeSource.setData, { apply(ogSetData, thisArg, args) { const [incomingData, ... restArgs] = args; const incomingCoordinates = incomingData.geometry.coordinates; const prevCoords = incomingCoordinates.length > 1 ? incomingCoordinates[incomingCoordinates.length - 2] : lastOldRouteCoord; const currentCoords = incomingCoordinates[incomingCoordinates.length - 1]; const [prevLng, prevLat] = prevCoords; const [thisLng, thisLat] = currentCoords; if (isTeleport([prevLat, prevLng], [thisLat, thisLng])) { const newCoordinatesArray = []; customRouteGeojsonData.geometry.coordinates.push(newCoordinatesArray); newCoordinatesArray.push([thisLng, thisLat]); addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]); updateModLayerSources(); } else { const lastCoordinatesArray = customRouteGeojsonData.geometry.coordinates[customRouteGeojsonData.geometry.coordinates.length - 1]; lastCoordinatesArray.push([thisLng, thisLat]); } return ogSetData.apply(thisArg, [customRouteGeojsonData, ... restArgs]) } }) } mapVDOM.state.getInitialData = new Proxy(mapVDOM.state.getInitialData, { apply(ogGetInitialData, thisArg, args) { const promise = ogGetInitialData.apply(thisArg, args); promise.then(() => { resolveTeleportsInOldRoute(); patchRouteSourceAndLayer(); }); return promise; } }); /* mapVDOM.state.setMarkerPosition = new Proxy(mapVDOM.state.setMarkerPosition, { apply(ogSetMarkerPosition, thisArg, args) { const result = ogSetMarkerPosition.apply(thisArg, args); if (isMapLoaded) { const routeSource = map.getSource('route'); routeSource.getData().then(async (routeSourceGeojsonData) => { const coordinates = routeSourceGeojsonData.geometry.coordinates; const prevCoords = coordinates.length > 1 ? coordinates[coordinates.length - 2] : lastOldRouteCoord; const currentCoords = coordinates[coordinates.length - 1]; const [prevLng, prevLat] = prevCoords; const [thisLng, thisLat] = currentCoords; if (isTeleport([prevLat, prevLng], [thisLat, thisLng])) { // Remove teleport line from route coordinates.pop(); addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]); routeSource.setData(routeSourceGeojsonData); updateModLayerSources(); } }); } return result; } }); */ { const tab = IRF.ui.panel.createTabFor( { ... GM.info, script: { ... GM.info.script, name: MOD_NAME } }, { tabName: MOD_NAME, style: ` .${cssClass('tab-content')} { & *, *::before, *::after { box-sizing: border-box; } & .${cssClass('field-group')} { margin-left: calc(var(${cssProp('field-group-indent')}, 0) * 1rem); margin-block: 1rem; gap: 0.25rem; display: flex; align-items: center; justify-content: space-between; & input[type="range"] { width: 50%; } } } `, className: cssClass('tab-content') } ); function makeFieldGroup({ id, label, indent = 0 }, renderInput) { const fieldGroupEl = document.createElement('div'); fieldGroupEl.className = cssClass('field-group'); if (indent > 0) { fieldGroupEl.style.setProperty(cssProp('field-group-indent'), indent); } const labelEl = document.createElement('label'); labelEl.textContent = label; fieldGroupEl.append(labelEl); const inputEl = renderInput({ id }); fieldGroupEl.append(inputEl); return fieldGroupEl; } tab.container.append( makeFieldGroup( { id: `${MOD_PREFIX}portal-icons-show-layer`, label: 'Portal Icons' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = settings.portalIcons.layer.show; inputEl.addEventListener('change', async () => { settings.portalIcons.layer.show = inputEl.checked; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}portal-icons-layer-opacity`, indent: 1, label: 'Opacity' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'range'; inputEl.className = IRF.ui.panel.styles.slider; inputEl.min = 0; inputEl.max = 1; inputEl.step = 0.05; inputEl.value = settings.portalIcons.layer.opacity; inputEl.addEventListener('input', async () => { let numberValue = Number.parseFloat(inputEl.value); if (Number.isNaN(numberValue)) { return; } numberValue = Math.min(Math.max(parseFloat(inputEl.min), numberValue), parseFloat(inputEl.max)); settings.portalIcons.layer.opacity = numberValue; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}teleport-lines-show-layer`, label: 'Teleport Lines' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = settings.teleportLines.layer.show; inputEl.addEventListener('change', async () => { settings.teleportLines.layer.show = inputEl.checked; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}teleport-lines-layer-opacity`, indent: 1, label: 'Opacity' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'range'; inputEl.className = IRF.ui.panel.styles.slider; inputEl.min = 0; inputEl.max = 1; inputEl.step = 0.05; inputEl.value = settings.teleportLines.layer.opacity; inputEl.addEventListener('input', async () => { let numberValue = Number.parseFloat(inputEl.value); if (Number.isNaN(numberValue)) { return; } numberValue = Math.min(Math.max(parseFloat(inputEl.min), numberValue), parseFloat(inputEl.max)); settings.teleportLines.layer.opacity = numberValue; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), ) } if (typeof unsafeWindow !== 'undefined') { unsafeWindow.mtpm = { get map() { return map }, get portalIconsLayer() { return map.getLayer(PORTAL_ICONS_LAYER_NAME); }, get teleportLinesLayer() { return map.getLayer(TELEPORT_LINES_LAYER_NAME); }, debug: { mockJumpToCoordsOnMap([lat, lng]) { mapVDOM.state.setMarkerPosition(lat, lng); } } }; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址