From fda820568e8b6091312bcbcb4412893d1d8e447a Mon Sep 17 00:00:00 2001 From: 4aiman Date: Thu, 25 Jun 2026 16:54:45 +0300 Subject: [PATCH] Compass map rotation, landscape layout, GPS placeholder - leaflet-rotate plugin: map rotates by compass heading via deviceorientation - Landscape detection: CSS viewport rotation when phone held sideways (gamma > 50) - Bottom nav hidden in landscape mode - GPS placeholder: pixel art overlay shown until GPS signal arrives - invalidateSize() after map init to prevent black screen --- frontend/css/styles.css | 48 +- frontend/img/gps-placeholder.png | Bin 0 -> 64538 bytes frontend/index.html | 2 + frontend/js/main.js | 204 +++- frontend/lib/leaflet-rotate.js | 1936 ++++++++++++++++++++++++++++++ src-tauri/Cargo.lock | 111 +- src-tauri/Cargo.toml | 3 +- src-tauri/tauri.conf.json | 4 +- 8 files changed, 2284 insertions(+), 24 deletions(-) create mode 100644 frontend/img/gps-placeholder.png create mode 100644 frontend/lib/leaflet-rotate.js diff --git a/frontend/css/styles.css b/frontend/css/styles.css index b15b6da..3fe7f16 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1059,9 +1059,53 @@ select { height: 100%; } -.rec-map { +.rec-panel { + flex-shrink: 0; +} + +.rec-map-wrap { flex: 1; - min-height: 300px; + min-height: 200px; + position: relative; +} +.rec-map { + position: absolute; + inset: 0; +} +.gps-placeholder { + position: absolute; + inset: 0; + z-index: 1000; + background: url('../img/gps-placeholder.png') center/cover; + display: flex; + align-items: center; + justify-content: center; +} +.gps-placeholder-text { + background: rgba(0,0,0,0.7); + color: var(--on-bg); + padding: 12px 24px; + border-radius: 8px; + font-size: 18px; +} + +/* Landscape: CSS rotation compensates physical rotation. + flex-direction stays column: element-bottom = visual-right after rotate. + No font-size changes — everything stays portrait-sized, just rearranged. */ +.rec-screen.landscape-right, +.rec-screen.landscape-left { + position: fixed; + width: 100vh; + height: 100vw; + top: calc((100vh - 100vw) / 2); + left: calc((100vw - 100vh) / 2); + z-index: 9999; +} +.rec-screen.landscape-right { + transform: rotate(-90deg); +} +.rec-screen.landscape-left { + transform: rotate(90deg); } .rec-stats { diff --git a/frontend/img/gps-placeholder.png b/frontend/img/gps-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..4437c5a83e68eda904bce32a111ad67534a4ffea GIT binary patch literal 64538 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

D8JJ}^RTOmN8Z;6)Raf3ySI*aF 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 Tk%iJU2>eWqt&Cn9UXA=e&f^xQ literal 0 HcmV?d00001 diff --git a/frontend/index.html b/frontend/index.html index 94fce5f..a182061 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,6 +13,7 @@ + @@ -21,6 +22,7 @@ + diff --git a/frontend/js/main.js b/frontend/js/main.js index 4653dbc..fa36e85 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1,6 +1,10 @@ // BalDav main app — pure JS, no framework, no bundler. // Depends on window.DB (db.js) and window.BMR (bmr.js) loaded before this script. +// Disable leaflet-rotate's compass control on all maps by default; +// only the recording map enables rotate: true explicitly. +if (L && L.Map) L.Map.mergeOptions({ rotateControl: false }); + const app = document.getElementById("app"); const UPDATE_BASE_URL = "https://4aiman.top/baldav"; @@ -176,6 +180,9 @@ async function init() { // Restore last screen (fromPopstate=true to avoid duplicate navStack push) renderScreen(navStack[navStack.length - 1], true); + // Initialize sync + try { await Sync.init(); if (Sync.userId) Sync.now(); } catch {} + // Check for updates in background (non-blocking) checkForUpdate(); } @@ -185,8 +192,9 @@ async function init() { // --------------------------------------------------------------------------- function renderScreen(name, fromPopstate) { - // Release screen wake lock when leaving recording screen + // Release screen wake lock and compass listener when leaving recording screen if (window._screenLock) { try { window._screenLock.release(); } catch {} window._screenLock = null; } + if (GPS.recording._compassCleanup) { GPS.recording._compassCleanup(); GPS.recording._compassCleanup = null; } const prevScreen = state.screen; state.screen = name; @@ -2733,23 +2741,32 @@ function renderRecordingInline(container) { container.innerHTML = `

-
- +
+
Синхронизация
+
+
+
${Sync.userId ? "Подключено" : "Не настроено"}
+
${Sync.userId ? (state.profile.sync_last_time || "Не синхронизировано") : "Привяжите устройство"}
+
+ +
+
+
+ +
+ + +
+
+ +
+
+ + +
+
+
О приложении
@@ -4599,6 +4716,57 @@ async function renderSettings(container) { }; } + // Sync handlers + const syncNowBtn = container.querySelector("#s-sync-now"); + if (syncNowBtn) { + syncNowBtn.onclick = async () => { + if (!Sync.userId) { + // Show setup + container.querySelector("#s-sync-setup").style.display = ""; + return; + } + syncNowBtn.textContent = "..."; + syncNowBtn.disabled = true; + const r = await Sync.now(); + container.querySelector("#s-sync-desc").textContent = `Отправлено: ${r.pushed}, получено: ${r.pulled}`; + syncNowBtn.textContent = "Синхр."; + syncNowBtn.disabled = false; + }; + } + container.querySelector("#s-sync-register")?.addEventListener("click", async () => { + try { + const code = await Sync.register(); + container.querySelector("#s-sync-status").textContent = "Подключено"; + container.querySelector("#s-sync-setup").style.display = "none"; + container.querySelector("#s-sync-info").style.display = ""; + container.querySelector("#s-sync-code-display").style.display = ""; + container.querySelector("#s-sync-code-display").textContent = code; + syncNowBtn.textContent = "Синхр."; + showToast("Аккаунт создан. Код: " + code); + } catch (e) { showToast("Ошибка: " + e.message); } + }); + container.querySelector("#s-sync-pair")?.addEventListener("click", async () => { + const code = container.querySelector("#s-sync-code").value.trim().toUpperCase(); + if (code.length !== 6) { showToast("Введите 6-символьный код"); return; } + try { + await Sync.pair(code); + container.querySelector("#s-sync-status").textContent = "Подключено"; + container.querySelector("#s-sync-setup").style.display = "none"; + container.querySelector("#s-sync-info").style.display = ""; + syncNowBtn.textContent = "Синхр."; + showToast("Устройство привязано!"); + await Sync.now(); + } catch (e) { showToast("Ошибка: " + e.message); } + }); + container.querySelector("#s-sync-show-code")?.addEventListener("click", async () => { + try { + const code = await Sync.newCode(); + const el = container.querySelector("#s-sync-code-display"); + el.style.display = ""; + el.textContent = code; + } catch (e) { showToast("Ошибка: " + e.message); } + }); + // App version info + update check try { const info = await window.__TAURI__.core.invoke("get_app_info"); diff --git a/frontend/lib/leaflet-rotate.js b/frontend/lib/leaflet-rotate.js new file mode 100644 index 0000000..6b8a343 --- /dev/null +++ b/frontend/lib/leaflet-rotate.js @@ -0,0 +1,1936 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +})((function () { 'use strict'; + + /** + * @external L.DomUtil + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/dom/DomUtil.js + */ + + const domUtilProto = L.extend({}, L.DomUtil); + + L.extend(L.DomUtil, { + + /** + * Resets the 3D CSS transform of `el` so it is + * translated by `offset` pixels and optionally + * scaled by `scale`. Does not have an effect if + * the browser doesn't support 3D CSS transforms. + * + * @param {HTMLElement} el + * @param {L.Point} offset + * @param {Number} scale + * @param {Number} bearing + * @param {L.Point} pivot + */ + setTransform: function(el, offset, scale, bearing, pivot) { + var pos = offset || new L.Point(0, 0); + + if (!bearing) { + offset = pos._round(); + return domUtilProto.setTransform.apply(this, arguments); + } + + pos = pos.rotateFrom(bearing, pivot); + + el.style[L.DomUtil.TRANSFORM] = + 'translate3d(' + pos.x + 'px,' + pos.y + 'px' + ',0)' + + (scale ? ' scale(' + scale + ')' : '') + + ' rotate(' + bearing + 'rad)'; + }, + + /** + * Sets the position of `el` to coordinates specified by + * `position`, using CSS translate or top/left positioning + * depending on the browser (used by Leaflet internally + * to position its layers). + * + * @param {HTMLElement} el + * @param {L.Point} point + * @param {Number} bearing + * @param {L.Point} pivot + * @param {Number} scale + */ + setPosition: function(el, point, bearing, pivot, scale) { + if (!bearing) { + return domUtilProto.setPosition.apply(this, arguments); + } + + /*eslint-disable */ + el._leaflet_pos = point; + /*eslint-enable */ + + if (L.Browser.any3d) { + L.DomUtil.setTransform(el, point, scale, bearing, pivot); + } else { + el.style.left = point.x + 'px'; + el.style.top = point.y + 'px'; + } + }, + + /** + * @constant radians = degrees × π/180° + */ + DEG_TO_RAD: Math.PI / 180, + + /** + * @constant degrees = radians × 180°/π + */ + RAD_TO_DEG: 180 / Math.PI, + + }); + + /** + * @external L.Draggable + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/dom/Draggable.js + */ + + /** + * A class for making DOM elements draggable (including touch support). + * Used internally for map and marker dragging. Only works for elements + * that were positioned with [`L.DomUtil.setPosition`](#domutil-setposition). + */ + + L.Draggable.include({ + + /** @TODO */ + // updateMapBearing: function(mapBearing) { + // this._mapBearing = mapBearing; + // }, + + }); + + /** + * @external L.Point + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/geometry/Point.js + */ + + L.extend(L.Point.prototype, { + + /** + * Rotate around (0,0) by applying the 2D rotation matrix: + * + * ⎡ x' ⎤ = ⎡ cos θ -sin θ ⎤ ⎡ x ⎤ + * ⎣ y' ⎦ ⎣ sin θ cos θ ⎦ ⎣ y ⎦ + * + * @param theta must be given in radians. + */ + rotate: function(theta) { + return this.rotateFrom(theta, new L.Point(0,0)) + }, + + /** + * Rotate around (pivot.x, pivot.y) by: + * + * 1. subtract (pivot.x, pivot.y) + * 2. rotate around (0, 0) + * 3. add (pivot.x, pivot.y) back + * + * same as `this.subtract(pivot).rotate(theta).add(pivot)` + * + * @param {Number} theta + * @param {L.Point} pivot + * + * @returns {L.Point} + */ + rotateFrom: function(theta, pivot) { + if (!theta) { return this; } + var sinTheta = Math.sin(theta); + var cosTheta = Math.cos(theta); + var cx = pivot.x, + cy = pivot.y; + var x = this.x - cx, + y = this.y - cy; + + return new L.Point( + x * cosTheta - y * sinTheta + cx, + x * sinTheta + y * cosTheta + cy + ); + }, + + }); + + /** + * @external L.DivOverlay + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/DivOverlay.js + */ + + const divOverlayProto = L.extend({}, L.DivOverlay.prototype); + + L.DivOverlay.include({ + + /** + * Update L.Popup and L.Tooltip anchor positions after + * the map is moved by calling `map.setBearing(theta)` + * + * @listens L.Map~rotate + */ + getEvents: function() { + return L.extend(divOverlayProto.getEvents.apply(this, arguments), { rotate: this._updatePosition }); + }, + + /** + * 0. update element anchor point (divOverlayProto v1.9.3) + * 1. rotate around anchor point (subtract anchor -> rotate point -> add anchor) + */ + _updatePosition: function() { + if (!this._map) { return; } + divOverlayProto._updatePosition.apply(this, arguments); + if (this._map && this._map._rotate && this._zoomAnimated) { + var anchor = this._getAnchor(); + var pos = L.DomUtil.getPosition(this._container).subtract(anchor); + L.DomUtil.setPosition(this._container, this._map.rotatedPointToMapPanePoint(pos).add(anchor)); + } + + }, + + }); + + /** + * @external L.Popup + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/Popup.js + */ + + const popupProto = L.extend({}, L.Popup.prototype); + + L.Popup.include({ + + /** + * 0. update element anchor point (popupProto v1.9.3) + * 1. rotate around anchor point (subtract anchor -> rotate point -> add anchor) + */ + _animateZoom: function(e) { + popupProto._animateZoom.apply(this, arguments); + if (this._map && this._map._rotate) { + var anchor = this._getAnchor(); + var pos = L.DomUtil.getPosition(this._container).subtract(anchor); + L.DomUtil.setPosition(this._container, this._map.rotatedPointToMapPanePoint(pos).add(anchor)); + } + }, + + /** + * Fix for L.popup({ keepInView = true }) + * + * @see https://github.com/fnicollet/Leaflet/pull/21 + */ + _adjustPan: function() { + if (!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress)) { return; } + + // We can endlessly recurse if keepInView is set and the view resets. + // Let's guard against that by exiting early if we're responding to our own autopan. + if (this._autopanning) { + this._autopanning = false; + return; + } + + var map = this._map, + marginBottom = parseInt(L.DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0, + containerHeight = this._container.offsetHeight + marginBottom, + containerWidth = this._containerWidth, + layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom); + + layerPos._add(L.DomUtil.getPosition(this._container)); + + /** @TODO use popupProto._adjustPan */ + // var containerPos = map.layerPointToContainerPoint(layerPos); + var containerPos = layerPos._add(this._map._getMapPanePos()), + padding = L.point(this.options.autoPanPadding), + paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding), + paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding), + size = map.getSize(), + dx = 0, + dy = 0; + + if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right + dx = containerPos.x + containerWidth - size.x + paddingBR.x; + } + if (containerPos.x - dx - paddingTL.x < 0) { // left + dx = containerPos.x - paddingTL.x; + } + if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom + dy = containerPos.y + containerHeight - size.y + paddingBR.y; + } + if (containerPos.y - dy - paddingTL.y < 0) { // top + dy = containerPos.y - paddingTL.y; + } + + // @namespace Map + // @section Popup events + // @event autopanstart: Event + // Fired when the map starts autopanning when opening a popup. + if (dx || dy) { + // Track that we're autopanning, as this function will be re-ran on moveend + if (this.options.keepInView) { + this._autopanning = true; + } + map + .fire('autopanstart') + .panBy([dx, dy]); + } + }, + + }); + + /** + * @external L.Tooltip + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/Tooltip.js + */ + + const tooltipProto = L.extend({}, L.Tooltip.prototype); + + L.Tooltip.include({ + + _animateZoom: function(e) { + if (!this._map._rotate) { + return tooltipProto._animateZoom.apply(this, arguments); + } + var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); + + pos = this._map.rotatedPointToMapPanePoint(pos); + this._setPosition(pos); + }, + + _updatePosition: function() { + if (!this._map._rotate) { + return tooltipProto._updatePosition.apply(this, arguments); + } + var pos = this._map.latLngToLayerPoint(this._latlng); + + pos = this._map.rotatedPointToMapPanePoint(pos); + this._setPosition(pos); + }, + + }); + + /** + * @external L.Icon + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/marker/Icon.js + */ + + L.extend({}, L.Icon.prototype); + + L.Icon.include({ + + _setIconStyles: function(img, name) { + var options = this.options; + var sizeOption = options[name + 'Size']; + + if (typeof sizeOption === 'number') { + sizeOption = [sizeOption, sizeOption]; + } + + var size = L.point(sizeOption), + anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor || + size && size.divideBy(2, true)); + + img.className = 'leaflet-marker-' + name + ' ' + (options.className || ''); + + if (anchor) { + img.style.marginLeft = (-anchor.x) + 'px'; + img.style.marginTop = (-anchor.y) + 'px'; + /** @TODO use iconProto._setIconStyles */ + img.style[L.DomUtil.TRANSFORM + "Origin"] = anchor.x + "px " + anchor.y + "px 0px"; + } + + if (size) { + img.style.width = size.x + 'px'; + img.style.height = size.y + 'px'; + } + }, + + }); + + /** + * @external L.Marker + * @external L.Handler.MarkerDrag + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/marker/Marker.js + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/marker/Marker.Drag.js + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/dom/Draggable.js + */ + + const markerProto = L.extend({}, L.Marker.prototype); + + L.Marker.mergeOptions({ + + /** + * Rotation of this marker in rad + * + * @type {Number} + */ + rotation: 0, + + /** + * Rotate this marker when map rotates + * + * @type {Boolean} + */ + rotateWithView: false, + + /** + * Scale of the marker icon + * + * @type {Number} + */ + scale: undefined, + + }); + + var markerDragProto; // retrived at runtime (see below: L.Marker::_initInteraction()) + + var MarkerDrag = { + + // _onDragStart: function() { + // if (!this._marker._map._rotate) { + // return markerDragProto._onDragStart.apply(this, arguments); + // } + // this._draggable.updateMapBearing(this._marker._map._bearing); + // }, + + _onDrag: function(e) { + var marker = this._marker, + /** @TODO use markerDragProto._onDrag */ + rotated_marker = marker.options.rotation || marker.options.rotateWithView, + shadow = marker._shadow, + iconPos = L.DomUtil.getPosition(marker._icon); + + /** @TODO use markerDragProto._onDrag */ + // update shadow position + if (!rotated_marker && shadow) { + L.DomUtil.setPosition(shadow, iconPos); + } + + /** @TODO use markerDragProto._onDrag */ + if (marker._map._rotate) { + // Reverse calculation from mapPane coordinates to rotatePane coordinates + iconPos = marker._map.mapPanePointToRotatedPoint(iconPos); + } + var latlng = marker._map.layerPointToLatLng(iconPos); + + marker._latlng = latlng; + e.latlng = latlng; + e.oldLatLng = this._oldLatLng; + + /** @TODO use markerDragProto._onDrag */ + if (rotated_marker) marker.setLatLng(latlng); // use `setLatLng` to presisit rotation. low efficiency + else marker.fire('move', e); // `setLatLng` will trig 'move' event. we imitate here. + + // @event drag: Event + // Fired repeatedly while the user drags the marker. + marker + .fire('drag', e); + }, + + _onDragEnd: function(e) { + if (this._marker._map._rotate) { + this._marker.update(); + } + markerDragProto._onDragEnd.apply(this, arguments); + }, + + }; + + L.Marker.include({ + + /** + * Update L.Marker anchor position after the map + * is moved by calling `map.setBearing(theta)` + * + * @listens L.Map~rotate + */ + getEvents: function() { + return L.extend(markerProto.getEvents.apply(this, arguments), { rotate: this.update }); + }, + + _initInteraction: function() { + var ret = markerProto._initInteraction.apply(this, arguments); + if (this.dragging && this.dragging.enabled() && this._map && this._map._rotate) { + // L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable + markerDragProto = markerDragProto || Object.getPrototypeOf(this.dragging); + this.dragging.disable(); + Object.assign(this.dragging, { + // _onDragStart: MarkerDrag._onDragStart.bind(this.dragging), + _onDrag: MarkerDrag._onDrag.bind(this.dragging), + _onDragEnd: MarkerDrag._onDragEnd.bind(this.dragging), + }); + this.dragging.enable(); + } + return ret; + }, + + _setPos: function(pos) { + + /** @TODO use markerProto._setPos */ + if (this._map._rotate) { + pos = this._map.rotatedPointToMapPanePoint(pos); + } + + /** @TODO use markerProto._setPos */ + var bearing = this.options.rotation || 0; + if (this.options.rotateWithView) { + bearing += this._map._bearing; + } + + /** @TODO use markerProto._setPos */ + if (this._icon) { + L.DomUtil.setPosition(this._icon, pos, bearing, pos, this.options.scale); + } + + /** @TODO use markerProto._setPos */ + if (this._shadow) { + L.DomUtil.setPosition(this._shadow, pos, bearing, pos, this.options.scale); + } + + this._zIndex = pos.y + this.options.zIndexOffset; + + this._resetZIndex(); + }, + + // _updateZIndex: function(offset) { + // if (!this._map._rotate) { + // return markerProto._updateZIndex.apply(this, arguments); + // } + // this._icon.style.zIndex = Math.round(this._zIndex + offset); + // }, + + setRotation: function(rotation) { + this.options.rotation = rotation; + this.update(); + }, + + }); + + /** + * @external L.GridLayer + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/tile/GridLayer.js + */ + + const gridLayerProto = L.extend({}, L.GridLayer.prototype); + + L.GridLayer.include({ + + /** + * Redraw L.TileLayer bounds after the map is + * moved by just calling `map.setBearing(theta)` + * + * @listens L.Map~rotate + */ + getEvents: function() { + var events = gridLayerProto.getEvents.apply(this, arguments); + if (this._map._rotate && !this.options.updateWhenIdle) { + if (!this._onRotate) { + this._onRotate = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this); + } + events.rotate = this._onRotate; + } + return events; + }, + + _getTiledPixelBounds: function(center) { + if (!this._map._rotate) { + return gridLayerProto._getTiledPixelBounds.apply(this, arguments); + } + + return this._map._getNewPixelBounds(center, this._tileZoom); + }, + + }); + + /** + * @external L.Renderer + * + * @see https://github.com/Leaflet/Leaflet/tree/v1.9.3/src/layer/vector/Renderer.js + */ + + const rendererProto = L.extend({}, L.Renderer.prototype); + + L.Renderer.include({ + + /** + * Redraw L.Canvas and L.SVG renderer bounds after the + * map is moved by just calling `map.setBearing(theta)` + * + * @listens L.Map~rotate + */ + getEvents: function() { + return L.extend(rendererProto.getEvents.apply(this, arguments), { rotate: this._update }); + }, + + /** + * Fix for `map.flyTo()` when `false === map.options.zoomAnimation` + * + * @see https://github.com/Leaflet/Leaflet/pull/8794 + */ + onAdd: function() { + rendererProto.onAdd.apply(this, arguments); + if (L.version <= "1.9.3") { + // always keep transform-origin as 0 0 + this._container.classList.add('leaflet-zoom-animated'); + } + }, + + /** + * @FIXME layer drifts on `map.setZoom()` (eg. zoom during animation) + * + * the main cause seems to be related to `this._updateTransform(path._center, path._zoom))` + * and `this._topLeft = this._map.layerPointToLatLng(this._bounds.min);` + * + * @example + * map.setZoom(2); + * path._renderer._update(); + * path._renderer._updateTransform(path._renderer._center, path._renderer._zoom); + * + * @see https://github.com/Leaflet/Leaflet/pull/8794 + * @see https://github.com/Leaflet/Leaflet/pull/8103 + * @see https://github.com/Leaflet/Leaflet/issues/7466 + * + * @TODO rechek this changes from leaflet@v1.9.3 + * + * @see https://github.com/Leaflet/Leaflet/compare/v1.7.0...v1.9.3 + */ + _updateTransform: function(center, zoom) { + if (!this._map._rotate) { + return rendererProto._updateTransform.apply(this, arguments); + } + /** + * @FIXME see path._renderer._reset(); + */ + var scale = this._map.getZoomScale(zoom, this._zoom), + offset = this._map._latLngToNewLayerPoint(this._topLeft, zoom, center); + + L.DomUtil.setTransform(this._container, offset, scale); + + }, + + // getEvents() { + // const events = { + // viewreset: this._reset, + // zoom: this._onZoom, + // moveend: this._update, + // zoomend: this._onZoomEnd + // }; + // if (this._zoomAnimated) { + // events.zoomanim = this._onAnimZoom; + // } + // return events; + // }, + + // _onAnimZoom(ev) { + // this._updateTransform(ev.center, ev.zoom); + // }, + + // _onZoom() { + // this._updateTransform(this._map.getCenter(), this._map.getZoom()); + // }, + + // _onZoomEnd() { + // for (const id in this._layers) { + // this._layers[id]._project(); + // } + // }, + + // _reset() { + // this._update(); + // this._updateTransform(this._center, this._zoom); + + // for (const id in this._layers) { + // this._layers[id]._reset(); + // } + // }, + + // _updatePaths() { + // for (const id in this._layers) { + // this._layers[id]._update(); + // } + // }, + + _update: function() { + if (!this._map._rotate) { + return rendererProto._update.apply(this, arguments); + } + // Update pixel bounds of renderer container (for positioning/sizing/clipping later) + // Subclasses are responsible of firing the 'update' event. + this._bounds = this._map._getPaddedPixelBounds(this.options.padding); + this._topLeft = this._map.layerPointToLatLng(this._bounds.min); + this._center = this._map.getCenter(); + this._zoom = this._map.getZoom(); + }, + + }); + + /** + * @external L.Map + * + * @see https://github.com/Leaflet/Leaflet/blob/v1.9.3/src/map/Map.js + */ + + const mapProto = L.extend({}, L.Map.prototype); + + L.Map.mergeOptions({ rotate: false, bearing: 0, }); + + L.Map.include({ + + /** + * @param {(HTMLElement|String)} id html selector + * @param {Object} [options={}] leaflet map options + */ + initialize: function(id, options) { + if (options.rotate) { + this._rotate = true; + this._bearing = 0; + } + mapProto.initialize.apply(this, arguments); + if(this.options.rotate){ + this.setBearing(this.options.bearing); + } + }, + + /** + * Given a pixel coordinate relative to the map container, + * returns the corresponding pixel coordinate relative to + * the [origin pixel](#map-getpixelorigin). + * + * @param {L.Point} point pixel screen coordinates + * @returns {L.Point} transformed pixel point + */ + containerPointToLayerPoint: function(point) { + if (!this._rotate) { + return mapProto.containerPointToLayerPoint.apply(this, arguments); + } + return L.point(point) + .subtract(this._getMapPanePos()) + .rotateFrom(-this._bearing, this._getRotatePanePos()) + .subtract(this._getRotatePanePos()); + }, + + /** + * Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin), + * returns the corresponding pixel coordinate relative to the map container. + * + * @param {L.Point} point pixel screen coordinates + * @returns {L.Point} transformed pixel point + */ + layerPointToContainerPoint: function(point) { + if (!this._rotate) { + return mapProto.layerPointToContainerPoint.apply(this, arguments); + } + return L.point(point) + .add(this._getRotatePanePos()) + .rotateFrom(this._bearing, this._getRotatePanePos()) + .add(this._getMapPanePos()); + }, + + /** + * Converts a coordinate from the rotated pane reference system + * to the reference system of the not rotated map pane. + * + * (rotatePane) --> (mapPane) + * (rotatePane) --> (norotatePane) + * + * @param {L.Point} point pixel screen coordinates + * @returns {L.Point} + * + * @since leaflet-rotate (v0.1) + */ + rotatedPointToMapPanePoint: function(point) { + return L.point(point) + .rotate(this._bearing) + ._add(this._getRotatePanePos()); + }, + + /** + * Converts a coordinate from the not rotated map pane reference system + * to the reference system of the rotated pane. + * + * (mapPane) --> (rotatePane) + * (norotatePane) --> (rotatePane) + * + * @param {L.Point} point pixel screen coordinates + * + * @since leaflet-rotate (v0.1) + */ + mapPanePointToRotatedPoint: function(point) { + return L.point(point) + ._subtract(this._getRotatePanePos()) + .rotate(-this._bearing); + }, + + // latLngToLayerPoint: function (latlng) { + // var projectedPoint = this.project(L.latLng(latlng))._round(); + // return projectedPoint._subtract(this.getPixelOrigin()); + // }, + + // latLngToContainerPoint: function (latlng) { + // return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng))); + // }, + + /** + * Given latlng bounds, returns the bounds in projected pixel + * relative to the map container. + * + * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/map/Map.js#L1114-L1135 + * + * @param {L.LatLngBounds} bounds + * @returns {L.Bounds} + * + * @since leaflet-rotate (v0.2) + */ + mapBoundsToContainerBounds: function (bounds) { + if (!this._rotate && mapProto.mapBoundsToContainerBounds) { + return mapProto.mapBoundsToContainerBounds.apply(this, arguments); + } + + // const nw = this.latLngToContainerPoint(bounds.getNorthWest()), + // ne = this.latLngToContainerPoint(bounds.getNorthEast()), + // sw = this.latLngToContainerPoint(bounds.getSouthWest()), + // se = this.latLngToContainerPoint(bounds.getSouthEast()); + + // same as `this.latLngToContainerPoint(latlng)` but with floating point precision + const origin = this.getPixelOrigin(); + const nw = this.layerPointToContainerPoint(this.project(bounds.getNorthWest())._subtract(origin)), + ne = this.layerPointToContainerPoint(this.project(bounds.getNorthEast())._subtract(origin)), + sw = this.layerPointToContainerPoint(this.project(bounds.getSouthWest())._subtract(origin)), + se = this.layerPointToContainerPoint(this.project(bounds.getSouthEast())._subtract(origin)); + + return L.bounds([ + L.point(Math.min(nw.x, ne.x, se.x, sw.x), Math.min(nw.y, ne.y, se.y, sw.y)), // [ minX, minY ] + L.point(Math.max(nw.x, ne.x, se.x, sw.x), Math.max(nw.y, ne.y, se.y, sw.y)) // [ maxX, maxY ] + ]); + }, + + /** + * Returns geographical bounds visible in the current map view + * + * @TODO find out if map bounds calculated by `L.Map::getBounds()` + * function should match the `rotatePane` or `norotatePane` bounds + * + * @see https://github.com/fnicollet/Leaflet/issues/7 + * + * @returns {L.LatLngBounds} + */ + getBounds: function() { + if (!this._rotate) { + return mapProto.getBounds.apply(this, arguments); + } + + // SEE: https://github.com/fnicollet/Leaflet/pull/22 + // + // var bounds = this.getPixelBounds(), + // sw = this.unproject(bounds.getBottomLeft()), + // ne = this.unproject(bounds.getTopRight()); + // return new LatLngBounds(sw, ne); + // + + // LatLngBounds' constructor automatically + // extends the bounds to fit the passed points + var size = this.getSize(); + return new L.LatLngBounds([ + this.containerPointToLatLng([0, 0]), // topleft + this.containerPointToLatLng([size.x, 0]), // topright + this.containerPointToLatLng([size.x, size.y]), // bottomright + this.containerPointToLatLng([0, size.y]), // bottomleft + ]); + }, + + /** + * Returns the bounds of the current map view in projected pixel + * coordinates (sometimes useful in layer and overlay implementations). + * + * @TODO find out if map bounds calculated by `L.Map::getPixelBounds()` + * function should match the `rotatePane` or `norotatePane` bounds + * + * @see https://github.com/fnicollet/Leaflet/issues/7 + * + * @returns {L.Bounds} + */ + // getPixelBounds(center, zoom) { + // // const topLeftPoint = map.containerPointToLayerPoint(this._getTopLeftPoint()); + // const topLeftPoint = this._getTopLeftPoint(center, zoom); + // return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize())); + // }, + + /** + * Change map rotation + * + * @param {number} theta map degrees + * + * @since leaflet-rotate (v0.1) + */ + setBearing: function(theta) { + if (!L.Browser.any3d || !this._rotate) { return; } + + var bearing = L.Util.wrapNum(theta, [0, 360]) * L.DomUtil.DEG_TO_RAD, + center = this._getPixelCenter(), + oldPos = this._getRotatePanePos().rotateFrom(-this._bearing, center), + newPos = oldPos.rotateFrom(bearing, center); + + // CSS transform + L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center); + + this._pivot = center; + this._bearing = bearing; + this._rotatePanePos = newPos; + + this.fire('rotate'); + }, + + /** + * Get current map rotation + * + * @returns {number} theta map degrees + * + * @since leaflet-rotate (v0.1) + */ + getBearing: function() { + return this._bearing * L.DomUtil.RAD_TO_DEG; + }, + + /** + * Creates a new [map pane](#map-pane) with the given name if it doesn't + * exist already, then returns it. The pane is created as a child of + * `container`, or as a child of the main map pane if not set. + * + * @param {String} name leaflet pane + * @param {HTMLElement} [container] parent element + * @returns {HTMLElement} pane container + */ + // createPane: function(name, container) { + // if (!this._rotate || name == 'mapPane') { + // return mapProto.createPane.apply(this, arguments); + // } + // // init "rotatePane" + // if (!this._rotatePane) { + // // this._pivot = this.getSize().divideBy(2); + // this._rotatePane = mapProto.createPane.call(this, 'rotatePane', this._mapPane); + // L.DomUtil.setPosition(this._rotatePane, new L.Point(0, 0), this._bearing, this._pivot); + // } + // return mapProto.createPane.call(this, name, container || this._rotatePane); + // }, + + /** + * Panes are DOM elements used to control the ordering of layers on + * the map. You can access panes with [`map.getPane`](#map-getpane) + * or [`map.getPanes`](#map-getpanes) methods. New panes can be created + * with the [`map.createPane`](#map-createpane) method. + * + * Every map has the following default panes that differ only in zIndex: + * + * - mapPane [HTMLElement = 'auto'] - Pane that contains all other map panes + * - tilePane [HTMLElement = 2] - Pane for tile layers + * - overlayPane [HTMLElement = 4] - Pane for overlays like polylines and polygons + * - shadowPane [HTMLElement = 5] - Pane for overlay shadows (e.g. marker shadows) + * - markerPane [HTMLElement = 6] - Pane for marker icons + * - tooltipPane [HTMLElement = 650] - Pane for tooltips. + * - popupPane [HTMLElement = 700] - Pane for popups. + */ + _initPanes: function() { + var panes = this._panes = {}; + this._paneRenderers = {}; + + this._mapPane = this.createPane('mapPane', this._container); + L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0)); + + if (this._rotate) { + this._rotatePane = this.createPane('rotatePane', this._mapPane); + this._norotatePane = this.createPane('norotatePane', this._mapPane); + // rotatePane + this.createPane('tilePane', this._rotatePane); + this.createPane('overlayPane', this._rotatePane); + // norotatePane + this.createPane('shadowPane', this._norotatePane); + this.createPane('markerPane', this._norotatePane); + this.createPane('tooltipPane', this._norotatePane); + this.createPane('popupPane', this._norotatePane); + } else { + this.createPane('tilePane'); + this.createPane('overlayPane'); + this.createPane('shadowPane'); + this.createPane('markerPane'); + this.createPane('tooltipPane'); + this.createPane('popupPane'); + } + + if (!this.options.markerZoomAnimation) { + L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide'); + L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide'); + } + }, + + /** + * Pans the map the minimum amount to make the `latlng` visible. Use + * padding options to fit the display to more restricted bounds. + * If `latlng` is already within the (optionally padded) display bounds, + * the map will not be panned. + * + * @see https://github.com/Raruto/leaflet-rotate/issues/18 + * + * @param {L.LatLng} latlng coordinates + * @param {Object} [options={}] padding options + * + * @returns {L.Map} current map instance + */ + panInside(latlng, options) { + if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) { + return mapProto.panInside.apply(this, arguments); + } + + options = options || {}; + + const paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), + paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), + /** @TODO use mapProto.panInside */ + // pixelPoint = this.project(latlng), + // pixelBounds = this.getPixelBounds(), + // pixelCenter = this.project(this.getCenter()), + rect = this._container.getBoundingClientRect(), + pixelPoint = this.latLngToContainerPoint(latlng), + pixelBounds = L.bounds([ L.point(rect), L.point(rect).add(this.getSize()) ]), + pixelCenter = pixelBounds.getCenter(), + // + paddedBounds = L.bounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]), + paddedSize = paddedBounds.getSize(); + + if (!paddedBounds.contains(pixelPoint)) { + this._enforcingBounds = true; + const centerOffset = pixelPoint.subtract(paddedBounds.getCenter()); + const offset = paddedBounds.extend(pixelPoint).getSize().subtract(paddedSize); + pixelCenter.x += centerOffset.x < 0 ? -offset.x : offset.x; + pixelCenter.y += centerOffset.y < 0 ? -offset.y : offset.y; + /** @TODO use mapProto.panInside */ + // this.panTo(this.unproject(pixelCenter), options); + this.panTo(this.containerPointToLatLng(pixelCenter), options); + // + this._enforcingBounds = false; + } + return this; + }, + + /** + * Pans the map to the closest view that would lie inside the given bounds + * (if it's not already), controlling the animation using the options specific, + * if any. + * + * @TODO check if map bounds calculated by `L.Map::panInsideBounds()` + * function should match the `rotatePane` or `norotatePane` bounds + * + * @see https://github.com/fnicollet/Leaflet/issues/7 + * + * @param {L.LatLngBounds} bounds coordinates + * @param {Object} [options] pan options + * @returns {L.Map} current map instance + */ + // panInsideBounds: function (bounds, options) { + // this._enforcingBounds = true; + // var center = this.getCenter(), + // newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds)); + // + // if (!center.equals(newCenter)) { + // this.panTo(newCenter, options); + // } + // + // this._enforcingBounds = false; + // return this; + // }, + + // adjust center for view to get inside bounds + // _limitCenter(center, zoom, bounds) { + // + // if (!bounds) { return center; } + // + // const centerPoint = this.project(center, zoom), + // viewHalf = this.getSize().divideBy(2), + // viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)), + // offset = this._getBoundsOffset(viewBounds, bounds, zoom); + // + // // If offset is less than a pixel, ignore. + // // This prevents unstable projections from getting into + // // an infinite loop of tiny offsets. + // if (Math.abs(offset.x) <= 1 && Math.abs(offset.y) <= 1) { + // return center; + // } + // + // return this.unproject(centerPoint.add(offset), zoom); + // }, + + // @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this + // Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto), + // but takes a bounds parameter like [`fitBounds`](#map-fitbounds). + // flyToBounds(bounds, options) { + // const target = this._getBoundsCenterZoom(bounds, options); + // return this.flyTo(target.center, target.zoom, options); + // }, + + // _getBoundsCenterZoom(bounds, options) { + // + // options = options || {}; + // bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds); + // + // const paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), + // paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]); + // + // let zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); + // + // zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom; + // + // if (zoom === Infinity) { + // return { center: bounds.getCenter(), zoom }; + // } + // + // return { center, zoom }; + // + // }, + + /** + * Returns the maximum zoom level on which the given bounds fit to the map + * view in its entirety. If `inside` (optional) is set to `true`, the method + * instead returns the minimum zoom level on which the map view fits into + * the given bounds in its entirety. + * + * @param {L.LatLngBounds} bounds + * @param {Boolean} [inside=false] + * @param {L.Point} [padding=[0,0]] + * + * @returns {Number} zoom level + */ + getBoundsZoom(bounds, inside, padding) { + if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) { + return mapProto.getBoundsZoom.apply(this, arguments); + } + + bounds = L.latLngBounds(bounds); + padding = L.point(padding || [0, 0]); + + let zoom = this.getZoom() || 0; + const min = this.getMinZoom(), + max = this.getMaxZoom(), + /** @TODO use mapProto.getBoundsZoom */ + // nw = bounds.getNorthWest(), + // se = bounds.getSouthEast(), + // size = this.getSize().subtract(padding), + // boundsSize = L.bounds(this.project(se, zoom), this.project(nw, zoom)).getSize(), + size = this.getSize().subtract(padding), + boundsSize = this.mapBoundsToContainerBounds(bounds).getSize(), + snap = this.options.zoomSnap, + scalex = size.x / boundsSize.x, + scaley = size.y / boundsSize.y, + scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley); + + zoom = this.getScaleZoom(scale, zoom); + + if (snap) { + zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level + zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap; + } + + return Math.max(min, Math.min(max, zoom)); + }, + + /** + * Layer point of the current center + * + * @returns {L.Point} layer center + */ + // _getCenterLayerPoint: function () { + // return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); + // }, + + /** + * Offset of the specified place to the current center in pixels + * + * @param {L.LatLng} latlng map coordinates + */ + _getCenterOffset: function(latlng) { + var centerOffset = mapProto._getCenterOffset.apply(this, arguments); + if (this._rotate) { + centerOffset = centerOffset.rotate(this._bearing); + } + return centerOffset; + }, + + /** + * @since leaflet-rotate (v0.1) + */ + _getRotatePanePos: function() { + return this._rotatePanePos || new L.Point(0, 0); + // return L.DomUtil.getPosition(this._rotatePane) || new L.Point(0, 0); + }, + + // _latLngToNewLayerPoint(latlng, zoom, center) { + // const topLeft = this._getNewPixelOrigin(center, zoom); + // return this.project(latlng, zoom)._subtract(topLeft); + //}, + + _getNewPixelOrigin: function(center, zoom) { + if (!this._rotate) { + return mapProto._getNewPixelOrigin.apply(this, arguments); + } + var viewHalf = this.getSize()._divideBy(2); + return this.project(center, zoom) + .rotate(this._bearing) + ._subtract(viewHalf) + ._add(this._getMapPanePos()) + ._add(this._getRotatePanePos()) + .rotate(-this._bearing) + ._round(); + }, + + /** + * @since leaflet-rotate (v0.2) + * + * @see src\layer\tile\GridLayer::_getTiledPixelBounds() + */ + _getNewPixelBounds: function(center, zoom) { + center = center || this.getCenter(); + zoom = zoom || this.getZoom(); + if (!this._rotate && mapProto._getNewPixelBounds) { + return mapProto._getNewPixelBounds.apply(this, arguments); + } + var mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(), + scale = this.getZoomScale(mapZoom, zoom), + pixelCenter = this.project(center, zoom).floor(), + size = this.getSize(), + halfSize = new L.Bounds([ + this.containerPointToLayerPoint([0, 0]).floor(), + this.containerPointToLayerPoint([size.x, 0]).floor(), + this.containerPointToLayerPoint([0, size.y]).floor(), + this.containerPointToLayerPoint([size.x, size.y]).floor() + ]).getSize().divideBy(scale * 2); + + return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize)); + }, + + /** + * @since leaflet-rotate (v0.2) + * + * @return {L.Point} map pivot point (center) + */ + _getPixelCenter: function() { + if (!this._rotate && mapProto._getPixelCenter) { + return mapProto._getPixelCenter.apply(this, arguments); + } + return this.getSize()._divideBy(2)._subtract(this._getMapPanePos()); + }, + + /** + * @since leaflet-rotate (v0.2) + * + * @see src\layer\vector\Renderer::_update() + */ + _getPaddedPixelBounds: function(padding) { + if (!this._rotate && mapProto._getPaddedPixelBounds) { + return mapProto._getPaddedPixelBounds.apply(this, arguments); + } + var p = padding, + size = this.getSize(), + padMin = size.multiplyBy(-p), + padMax = size.multiplyBy(1 + p); + //min = this.containerPointToLayerPoint(size.multiplyBy(-p)).round(); + + return new L.Bounds([ + this.containerPointToLayerPoint([padMin.x, padMin.y]).floor(), + this.containerPointToLayerPoint([padMin.x, padMax.y]).floor(), + this.containerPointToLayerPoint([padMax.x, padMin.y]).floor(), + this.containerPointToLayerPoint([padMax.x, padMax.y]).floor() + ]); + }, + + _handleGeolocationResponse: function(pos) { + if (!this._container._leaflet_id) { return; } + + var lat = pos.coords.latitude, + lng = pos.coords.longitude, + /** @TODO use mapProto._handleGeolocationResponse */ + hdg = pos.coords.heading, + latlng = new L.LatLng(lat, lng), + bounds = latlng.toBounds(pos.coords.accuracy), + options = this._locateOptions; + + if (options.setView) { + var zoom = this.getBoundsZoom(bounds); + this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); + } + + var data = { + latlng: latlng, + bounds: bounds, + timestamp: pos.timestamp, + /** @TODO use mapProto._handleGeolocationResponse */ + heading: hdg + }; + + for (var i in pos.coords) { + if (typeof pos.coords[i] === 'number') { + data[i] = pos.coords[i]; + } + } + + // @event locationfound: LocationEvent + // Fired when geolocation (using the [`locate`](#map-locate) method) + // went successfully. + this.fire('locationfound', data); + }, + + /** + * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/geo/LatLngBounds.js#L253-L264 + * + * @param {L.Bounds} points + * @returns {L.Bounds} + */ + // toCircumscribedBounds(points) { + // var minX = points.reduce(function (pv, v) { return Math.min(pv, v.x); }, points[0].x), + // maxX = points.reduce(function (pv, v) { return Math.max(pv, v.x); }, points[0].x), + // minY = points.reduce(function (pv, v) { return Math.min(pv, v.y); }, points[0].y), + // maxY = points.reduce(function (pv, v) { return Math.max(pv, v.y); }, points[0].y); + // + // return L.bounds(L.point(minX, minY), L.point(maxX, maxY)); + // }, + + }); + + /** + * Rotates the map according to a smartphone's compass. + * + * @typedef L.Map.CompassBearing + */ + + L.Map.CompassBearing = L.Handler.extend({ + + initialize: function(map) { + this._map = map; + /** @see https://caniuse.com/?search=DeviceOrientation */ + if ('ondeviceorientationabsolute' in window) { + this.__deviceOrientationEvent = 'deviceorientationabsolute'; + } else if('ondeviceorientation' in window) { + this.__deviceOrientationEvent = 'deviceorientation'; + } + this._throttled = L.Util.throttle(this._onDeviceOrientation, 100, this); + }, + + addHooks: function() { + if (this._map._rotate && this.__deviceOrientationEvent) { + L.DomEvent.on(window, this.__deviceOrientationEvent, this._throttled, this); + } else { + // L.Map.CompassBearing handler will be automatically + // disabled if device orientation is not supported. + this.disable(); + } + }, + + removeHooks: function() { + if (this._map._rotate && this.__deviceOrientationEvent) { + L.DomEvent.off(window, this.__deviceOrientationEvent, this._throttled, this); + } + }, + + /** + * `DeviceOrientationEvent.absolute` - Indicates whether the device is providing absolute + * orientation values (relatives to Magnetic North) or + * using some arbitrary frame determined by the device. + * + * `DeviceOrientationEvent.alpha` - Returns the rotation of the device around the Z axis; + * that is, the number of degrees by which the device is + * being twisted around the center of the screen. + * + * `window.orientation` - Returns the screen orientation in degrees (in 90-degree increments) + * of the viewport relative to the device's natural orientation. + * Its only possible values are -90, 0, 90, and 180. Positive + * values are counterclockwise; negative values are clockwise. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/absolute + * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/alpha + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/orientation + */ + _onDeviceOrientation: function(e) { + var angle = e.webkitCompassHeading || e.alpha; + var deviceOrientation = 0; + + // Safari iOS + if (!e.absolute && e.webkitCompassHeading) { + angle = 360 - angle; + } + + // Older browsers + if (!e.absolute && 'undefined' !== typeof window.orientation) { + deviceOrientation = window.orientation; + } + + this._map.setBearing(angle - deviceOrientation); + }, + + }); + + /** + * Add Compass bearing handler to L.Map (disabled unless `window.DeviceOrientationEvent` is set). + * + * @property {L.Map.CompassBearing} compassBearing + */ + L.Map.addInitHook('addHandler', 'compassBearing', L.Map.CompassBearing); + + /** + * Triggers `invalidateResize` when the map's DOM container mutates. + * + * @typedef L.Map.ContainerMutation + */ + + /** + * @TODO check again this file after leaflet v1.9.3 (eg. L.Browser.mutation). + * Mutation Observer support will likely be added by default in next releases. + */ + + L.Map.mergeOptions({ + + /** + * Whether the map uses mutation observers to + * detect changes in its container and trigger + * `invalidateSize`. Disabled by default due to + * support not being available in all web browsers. + * + * @type {Boolean} + * + * @see https://developer.mozilla.org/docs/Web/API/MutationObserver + */ + trackContainerMutation: false + + }); + + L.Map.ContainerMutation = L.Handler.extend({ + + addHooks: function() { + // if (!L.Browser.mutation) { return; } + if (!this._observer) { + this._observer = new MutationObserver(L.Util.bind(this._map.invalidateSize, this._map)); + } + this._observer.observe(this._map.getContainer(), { + childList: false, + attributes: true, + characterData: false, + subtree: false, + attributeFilter: ['style'] + }); + }, + + removeHooks: function() { + // if (!L.Browser.mutation) { return; } + this._observer.disconnect(); + }, + + }); + + /** + * Add Container mutation handler to L.Map (disabled unless `trackContainerMutation` is set). + * + * @property {L.Map.ContainerMutation} trackContainerMutation + */ + L.Map.addInitHook('addHandler', 'trackContainerMutation', L.Map.ContainerMutation); + + /** + * TouchGestures is both TouchZoom plus TouchRotate + * + * @see https://github.com/fnicollet/Leaflet/commit/a77af51a6b10f308d1b9a16552091d1d0aee8834 + * @see https://github.com/Leaflet/Leaflet/blob/v1.9.3/src/map/handler/Map.TouchZoom.js + * + * @typedef L.Map.TouchGestures + */ + + L.Map.mergeOptions({ + + /** + * Set it to false if you don't want the map to + * zoom beyond min/max zoom and then bounce back + * when pinch-zooming. + * + * @type {Boolean} + */ + bounceAtZoomLimits: true, + + }); + + L.Map.TouchGestures = L.Handler.extend({ + + initialize: function(map) { + this._map = map; + this.rotate = !!this._map.options.touchRotate; + this.zoom = !!this._map.options.touchZoom; + }, + + addHooks: function() { + L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this); + }, + + removeHooks: function() { + L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this); + }, + + _onTouchStart: function(e) { + var map = this._map; + + if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating) { return; } + + var p1 = map.mouseEventToContainerPoint(e.touches[0]), + p2 = map.mouseEventToContainerPoint(e.touches[1]), + vector = p1.subtract(p2); + + this._centerPoint = map.getSize()._divideBy(2); + this._startLatLng = map.containerPointToLatLng(this._centerPoint); + + if (this.zoom) { + if (map.options.touchZoom !== 'center') { + this._pinchStartLatLng = map.containerPointToLatLng(p1.add(p2)._divideBy(2)); + } + this._startDist = p1.distanceTo(p2); + this._startZoom = map.getZoom(); + this._zooming = true; + } else { + this._zooming = false; + } + + if (this.rotate) { + this._startTheta = Math.atan(vector.x / vector.y); + this._startBearing = map.getBearing(); + if (vector.y < 0) { this._startBearing += 180; } + this._rotating = true; + } else { + this._rotating = false; + } + + this._moved = false; + + map._stop(); + + L.DomEvent + .on(document, 'touchmove', this._onTouchMove, this) + .on(document, 'touchend touchcancel', this._onTouchEnd, this); + + L.DomEvent.preventDefault(e); + }, + + _onTouchMove: function(e) { + if (!e.touches || e.touches.length !== 2 || !(this._zooming || this._rotating)) { return; } + + var map = this._map, + p1 = map.mouseEventToContainerPoint(e.touches[0]), + p2 = map.mouseEventToContainerPoint(e.touches[1]), + vector = p1.subtract(p2), + scale = p1.distanceTo(p2) / this._startDist, + delta; + + if (this._rotating) { + var theta = Math.atan(vector.x / vector.y); + var bearingDelta = (theta - this._startTheta) * L.DomUtil.RAD_TO_DEG; + if (vector.y < 0) { bearingDelta += 180; } + if (bearingDelta) { + /** + * @TODO the pivot should be the last touch point, + * but zoomAnimation manages to overwrite the rotate + * pane position. Maybe related to #3529. + * + * @see https://github.com/Leaflet/Leaflet/pull/3529 + * @see https://github.com/fnicollet/Leaflet/commit/a77af51a6b10f308d1b9a16552091d1d0aee8834 + */ + map.setBearing(this._startBearing - bearingDelta); + } + } + + if (this._zooming) { + this._zoom = map.getScaleZoom(scale, this._startZoom); + + if (!map.options.bounceAtZoomLimits && ( + (this._zoom < map.getMinZoom() && scale < 1) || + (this._zoom > map.getMaxZoom() && scale > 1))) { + this._zoom = map._limitZoom(this._zoom); + } + + if (map.options.touchZoom === 'center') { + this._center = this._startLatLng; + if (scale === 1) { return; } + } else { + // Get delta from pinch to center, so centerLatLng is delta applied to initial pinchLatLng + delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint); + if (scale === 1 && delta.x === 0 && delta.y === 0) { return; } + + var alpha = -map.getBearing() * L.DomUtil.DEG_TO_RAD; + + this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(delta.rotate(alpha))); + } + + } + + if (!this._moved) { + map._moveStart(true, false); + this._moved = true; + } + + L.Util.cancelAnimFrame(this._animRequest); + + var moveFn = map._move.bind(map, this._center, this._zoom, { pinch: true, round: false }, undefined); + this._animRequest = L.Util.requestAnimFrame(moveFn, this, true); + + L.DomEvent.preventDefault(e); + }, + + _onTouchEnd: function() { + if (!this._moved || !(this._zooming || this._rotating)) { + this._zooming = false; + return; + } + + this._zooming = false; + this._rotating = false; + L.Util.cancelAnimFrame(this._animRequest); + + L.DomEvent + .off(document, 'touchmove', this._onTouchMove, this) + .off(document, 'touchend touchcancel', this._onTouchEnd, this); + + if (this.zoom) { + // Pinch updates GridLayers' levels only when zoomSnap is off, so zoomSnap becomes noUpdate. + if (this._map.options.zoomAnimation) { + this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.zoomSnap); + } else { + this._map._resetView(this._center, this._map._limitZoom(this._zoom)); + } + } + }, + + }); + + /** + * Add Touch Gestures handler (enabled unless `touchGestures` is unset). + * + * @property {L.Map.TouchGestures} touchGestures + */ + L.Map.addInitHook('addHandler', 'touchGestures', L.Map.TouchGestures); + + /** + * Rotates the map on two-finger (touch devices). + * + * @typedef L.Map.TouchRotate + */ + + L.Map.mergeOptions({ + + /** + * Whether the map can be rotated with a two-finger rotation gesture + * + * @type {Boolean} + */ + touchRotate: false, + + }); + + L.Map.TouchRotate = L.Handler.extend({ + + addHooks: function() { + this._map.touchGestures.enable(); + this._map.touchGestures.rotate = true; + }, + + removeHooks: function() { + this._map.touchGestures.rotate = false; + }, + + }); + + /** + * Add Touch Rotate handler (disabled unless `touchGestures` is set). + * + * @property {L.Map.TouchGestures} touchGestures + */ + L.Map.addInitHook('addHandler', 'touchRotate', L.Map.TouchRotate); + + /** + * Rotates the map on shift key + mousewheel scrolling (desktop). + * + * @typedef L.Map.ShiftKeyRotate + */ + + L.Map.mergeOptions({ + + /** + * Whether the map can be rotated with shift + wheel scroll + * @type {Boolean} + */ + shiftKeyRotate: true, + + }); + + L.Map.ShiftKeyRotate = L.Handler.extend({ + + addHooks: function() { + L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this); + // this._map.shiftKeyRotate.enable(); + this._map.shiftKeyRotate.rotate = true; + }, + + removeHooks: function() { + L.DomEvent.off(this._map._container, "wheel", this._handleShiftScroll, this); + this._map.shiftKeyRotate.rotate = false; + }, + + _handleShiftScroll: function(e) { + if (e.shiftKey) { + e.preventDefault(); + this._map.scrollWheelZoom.disable(); + this._map.setBearing((this._map._bearing * L.DomUtil.RAD_TO_DEG) + Math.sign(e.deltaY) * 5); + } else { + this._map.scrollWheelZoom.enable(); + } + }, + + }); + + /** + * Add ShiftKey handler to L.Map (enabled unless `shiftKeyRotate` is unset). + * + * @property {L.Map.ShiftKeyRotate} shiftKeyRotate + */ + L.Map.addInitHook('addHandler', 'shiftKeyRotate', L.Map.ShiftKeyRotate); + + // decrease `scrollWheelZoom` handler priority over `shiftKeyRotate` handler + L.Map.addInitHook(function() { + if (this.scrollWheelZoom.enabled() && this.shiftKeyRotate.enabled()) { + this.scrollWheelZoom.disable(); + this.scrollWheelZoom.enable(); + } + }); + + /** + * Adds pinch zoom rotation on mobile browsers + * + * @see https://github.com/Leaflet/Leaflet/blob/v1.9.3/src/map/handler/Map.TouchZoom.js + * + * @external L.Map.TouchZoom + */ + + L.Map.mergeOptions({ + + /** + * Whether the map can be zoomed by touch-dragging + * with two fingers. If passed `'center'`, it will + * zoom to the center of the view regardless of + * where the touch events (fingers) were. Enabled + * for touch-capable web browsers. + * + * @type {(Boolean|String)} + */ + touchZoom: L.Browser.touch, + + /** + * @TODO check if this is a duplicate of `L.Map.TouchGestures::bounceAtZoomLimits` + * + * Set it to false if you don't want the map to + * zoom beyond min/max zoom and then bounce back + * when pinch-zooming. + * + * @type {Boolean} + */ + bounceAtZoomLimits: false, + + }); + + L.Map.TouchZoom = L.Handler.extend({ + + addHooks: function() { + L.DomUtil.addClass(this._map._container, 'leaflet-touch-zoom'); + this._map.touchGestures.enable(); + this._map.touchGestures.zoom = true; + }, + + removeHooks: function() { + L.DomUtil.removeClass(this._map._container, 'leaflet-touch-zoom'); + this._map.touchGestures.zoom = false; + }, + + }); + + /** + * Add Touch Zoom handler (disabled unless `L.Browser.touch` is set). + * + * @property {L.Map.TouchGestures} touchGestures + */ + L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom); + + /** + * A tri-state control for map rotation, states are: + * + * - Locked (default) + * - Unlocked (user can pinch-rotate) + * - Follow (rotation follows device orientation, if available) + * + * @typedef L.Control.Rotate + */ + + L.Control.Rotate = L.Control.extend({ + + options: { + position: 'topleft', + closeOnZeroBearing: true + }, + + onAdd: function(map) { + var container = this._container = L.DomUtil.create('div', 'leaflet-control-rotate leaflet-bar'); + + // this.button = L.Control.Zoom.prototype._createButton.call(this, 'R', 'leaflet-control-rotate', 'leaflet-control-rotate', container, this._toggleLock); + + var arrow = this._arrow = L.DomUtil.create('span', 'leaflet-control-rotate-arrow'); + + arrow.style.backgroundImage = `url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")`; + arrow.style.cursor = 'grab'; + arrow.style.display = 'block'; + arrow.style.width = '100%'; + arrow.style.height = '100%'; + arrow.style.backgroundRepeat = 'no-repeat'; + arrow.style.backgroundPosition = '50%'; + + // Copy-pasted from L.Control.Zoom + var link = this._link = L.DomUtil.create('a', 'leaflet-control-rotate-toggle', container); + link.appendChild(arrow); + link.href = '#'; + link.title = 'Rotate map'; + + L.DomEvent + .on(link, 'dblclick', L.DomEvent.stopPropagation) + .on(link, 'mousedown', this._handleMouseDown, this) + .on(link, 'click', L.DomEvent.stop) + .on(link, 'click', this._cycleState, this) + .on(link, 'click', this._refocusOnMap, this); + + if (!L.Browser.any3d) { + L.DomUtil.addClass(link, 'leaflet-disabled'); + } + + this._restyle(); + + map.on('rotate', this._restyle, this); + + // State flag + this._follow = false; + this._canFollow = false; + + if (this.options.closeOnZeroBearing && map.getBearing() === 0) { + container.style.display = 'none'; + } + + return container; + }, + + onRemove: function(map) { + map.off('rotate', this._restyle, this); + }, + + _handleMouseDown: function(e) { + L.DomEvent.stop(e); + this.dragging = true; + this.dragstartX = e.pageX; + this.dragstartY = e.pageY; + L.DomEvent + .on(document, 'mousemove', this._handleMouseDrag, this) + .on(document, 'mouseup', this._handleMouseUp, this); + }, + + _handleMouseUp: function(e) { + L.DomEvent.stop(e); + this.dragging = false; + + L.DomEvent + .off(document, 'mousemove', this._handleMouseDrag, this) + .off(document, 'mouseup', this._handleMouseUp, this); + }, + + _handleMouseDrag: function(e) { + if (!this.dragging) { return; } + var deltaX = e.clientX - this.dragstartX; + this._map.setBearing(deltaX); + }, + + _cycleState: function(ev) { + if (!this._map) { + return; + } + + var map = this._map; + + // Touch mode + if (!map.touchRotate.enabled() && !map.compassBearing.enabled()) { + map.touchRotate.enable(); + } + + // Compass mode + else if (!map.compassBearing.enabled()) { + map.touchRotate.disable(); + map.compassBearing.enable(); + } + + // Locked mode + else { + map.compassBearing.disable(); + map.setBearing(0); + if (this.options.closeOnZeroBearing) { + map.touchRotate.enable(); + } + } + this._restyle(); + }, + + _restyle: function() { + if (!this._map.options.rotate) { + L.DomUtil.addClass(this._link, 'leaflet-disabled'); + } else { + var map = this._map; + var bearing = map.getBearing(); + + this._arrow.style.transform = 'rotate(' + bearing + 'deg)'; + + if (bearing && this.options.closeOnZeroBearing) { + this._container.style.display = 'block'; + } + + // Compass mode + if (map.compassBearing.enabled()) { + this._link.style.backgroundColor = 'orange'; + } + + // Touch mode + else if (map.touchRotate.enabled()) { + this._link.style.backgroundColor = null; + } + + // Locked mode + else { + this._link.style.backgroundColor = 'grey'; + if (0 === bearing && this.options.closeOnZeroBearing) { + this._container.style.display = 'none'; + } + } + } + }, + + }); + + L.control.rotate = function(options) { + return new L.Control.Rotate(options); + }; + + L.Map.mergeOptions({ + rotateControl: true, + }); + + L.Map.addInitHook(function() { + if (this.options.rotateControl) { + var options = typeof this.options.rotateControl === 'object' ? this.options.rotateControl : {}; + this.rotateControl = L.control.rotate(options); + this.addControl(this.rotateControl); + } + }); + +})); +//# sourceMappingURL=leaflet-rotate-src.js.map diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f7c6bac..26e84ef 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -139,7 +139,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "baldav" -version = "0.1.244" +version = "0.1.262" dependencies = [ "btleplug", "lazy_static", @@ -151,6 +151,7 @@ dependencies = [ "tauri-plugin-sql", "tokio", "tokio-stream", + "ureq", "uuid", ] @@ -3198,6 +3199,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -3306,6 +3321,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -4893,6 +4943,41 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -4924,6 +5009,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5198,6 +5289,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5447,6 +5547,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c4ea30a..9ed4f75 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "baldav" -version = "0.1.244" +version = "0.1.262" edition = "2021" [lib] @@ -18,6 +18,7 @@ serde_json = "1" rusqlite = { version = "0.31", features = ["bundled"] } lazy_static = "1" +ureq = "3" [target.'cfg(not(target_os = "android"))'.dependencies] btleplug = { version = "0.11", features = ["serde"] } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ca69eb8..8f1b9a1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicegui/tauri-v2/refs/heads/main/tooling/cli/schema.json", "productName": "BalDav", - "version": "0.1.244", + "version": "0.1.262", "identifier": "dev.baldav.app", "build": { "frontendDist": "../frontend" @@ -9,7 +9,7 @@ "app": { "withGlobalTauri": true, "security": { - "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://4aiman.top https://*.tile.openstreetmap.org https://*.tile.thunderforest.com https://api.open-meteo.com; img-src 'self' blob: data: https://*.tile.openstreetmap.org https://*.tile.thunderforest.com" + "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://4aiman.top https://*.tile.openstreetmap.org https://*.tile.thunderforest.com https://api.open-meteo.com http://192.168.1.1:88; img-src 'self' blob: data: https://*.tile.openstreetmap.org https://*.tile.thunderforest.com" }, "windows": [ {