diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5a938ce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "useTabs": false +} diff --git a/create-xdc.sh b/create-xdc.sh index f06d683..268c86c 100755 --- a/create-xdc.sh +++ b/create-xdc.sh @@ -14,7 +14,7 @@ case "$1" in esac rm "$PACKAGE_NAME.xdc" 2> /dev/null -zip -9 --recurse-paths "$PACKAGE_NAME.xdc" * --exclude LICENSE README.md webxdc.js webxdc.d.ts icon.png "*screenshot*" "*marker-*png" "*-src*" "*.map" "*.sh" "*.xdc" *.DS_Store +zip -9 --recurse-paths "$PACKAGE_NAME.xdc" * --exclude LICENSE README.md webxdc.js webxdc.d.ts "*screenshot*" "*marker-*png" "*-src*" "*.map" "*.sh" "*.xdc" *.DS_Store echo "success, archive contents:" unzip -l "$PACKAGE_NAME.xdc" diff --git a/icon.png b/icon.png index 85861e7..44adc27 100644 Binary files a/icon.png and b/icon.png differ diff --git a/images/settings.svg b/images/settings.svg new file mode 100644 index 0000000..817c782 --- /dev/null +++ b/images/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html index b1a01e1..3a4fca3 100644 --- a/index.html +++ b/index.html @@ -1,126 +1,76 @@ - - - - - - - - - - -
- - + + + + + + + + + +
+ + + + + +
+

Contacts

+
+
No contacts yet
+
+
+ +
+

Points of Interest

+
+
No POIs yet
+
+
+ + + +
+

Settings

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + diff --git a/index.js b/index.js index 32f788b..23ba364 100644 --- a/index.js +++ b/index.js @@ -1,144 +1,517 @@ - -// set up map - -var map = L.map('map', { - doubleClickZoom: true, - zoomControl: false, // added manually below - tapHold: true - }); +const map = L.map('map', { + doubleClickZoom: true, + zoomControl: false, // added manually below + tapHold: true, +}); if (localStorage.getItem('map.lat') === null) { map.setView([30, -30], 3); } else { - map.setView([localStorage.getItem('map.lat'), localStorage.getItem('map.lng')], localStorage.getItem('map.zoom')); + map.setView( + [localStorage.getItem('map.lat'), localStorage.getItem('map.lng')], + localStorage.getItem('map.zoom') + ); } map.attributionControl.setPrefix(''); -L.control.scale({position: 'bottomleft'}).addTo(map); -L.control.zoom({position: 'topright'}).addTo(map); +L.control.scale({ position: 'bottomleft' }).addTo(map); +L.control.zoom({ position: 'topright' }).addTo(map); + +if (/wv/.test(navigator.userAgent) && /Android/.test(navigator.userAgent)) { + document.body.classList.add('android-webview'); +} + +const select = document.getElementById('mapTileServiceSelector'); + +for (const serviceKey in mapServices) { + const option = document.createElement('option'); + option.value = serviceKey; + option.textContent = serviceKey; + select.appendChild(option); +} + +let tileLayer = null; +let annotationLayer = null; + +function addMapService(map, serviceKey) { + localStorage.setItem('map.tileService', serviceKey); + console.log('Switching tile service to:', serviceKey); + const service = mapServices[serviceKey]; + if (!service) return; + + if (tileLayer && map.hasLayer(tileLayer)) { + map.removeLayer(tileLayer); + } + if (annotationLayer && map.hasLayer(annotationLayer)) { + map.removeLayer(annotationLayer); + } + const subdomains = service.subdomains || []; + // desktop does not allow electron to access internet directly + const protocol = navigator.userAgent.includes('Electron') + ? 'maps:' + : 'https:'; + tileLayer = L.tileLayer(service.url.replace('https:', protocol), { + maxZoom: service.options.maxZoom, + attribution: service.options.attribution, + tms: service.options.tms || false, + subdomains: subdomains, //service.subdomains.join(',') + }).addTo(map); + + if (service.annotationLayer) { + const annotationLayersubdomains = + service.annotationLayer.subdomains || []; + annotationLayer = L.tileLayer( + service.annotationLayer.url.replace('https:', protocol), + { + maxZoom: service.annotationLayer.options.maxZoom, + tms: service.annotationLayer.options.tms || false, + subdomains: annotationLayersubdomains, //service.subdomains.join(',') + } + ).addTo(map); + } +} + +select.addEventListener('change', function () { + const selectedService = this.value; + console.log('Selected tile service:', selectedService); + addMapService(map, selectedService); +}); -let url = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; +let tileServiceKey = defaultServiceKey; +if (localStorage.getItem('map.tileService') !== null) { + tileServiceKey = localStorage.getItem('map.tileService'); + console.log('Restoring tile service:', tileServiceKey); +} else { + console.log('Using default tile service:', defaultServiceKey); +} -let tileLayer = L.tileLayer(url, { - maxZoom: 19, - attribution: "© OpenStreetMap" -}).addTo(map); +select.value = tileServiceKey; +select.dispatchEvent(new Event('change')); -var pinIcon = L.icon({ +const pinIcon = L.icon({ iconUrl: 'images/pin-icon.png', iconRetinaUrl: 'images/pin-icon-2x.png', - iconSize: [12, 29], // size of the icon - iconAnchor: [6, 29], // point of the icon which will correspond to marker's location - popupAnchor: [0, -29] // point from which the popup should open relative to the iconAnchor + iconSize: [12, 29], // size of the icon + iconAnchor: [6, 29], // point of the icon which will correspond to marker's location + popupAnchor: [0, -29], // point from which the popup should open relative to the iconAnchor }); -var tracks = {}; -var initDone = false; +// Overlay management +let contactOverlayVisible = false; +let poiOverlayVisible = false; +let settingsOverlayVisible = false; +const contactsData = new Map(); // Store contact data for the overlay +const poiData = new Map(); // Store POI data for the overlay + +// Settings management +let showContactToggle = false; +let showPoiToggle = false; + +// DOM elements +const contactOverlay = document.getElementById('contactsOverlay'); +const poiOverlay = document.getElementById('poiOverlay'); +const settingsOverlay = document.getElementById('settingsOverlay'); +const toggleBtn = document.getElementById('toggleOverlay'); +const poiToggleBtn = document.getElementById('togglePoiOverlay'); +const settingsBtn = document.getElementById('settingsButton'); +const showContactToggleCheckbox = document.getElementById('showContactToggle'); +const showPoiToggleCheckbox = document.getElementById('showPoiToggle'); + +// Load settings from localStorage +function loadSettings() { + showContactToggle = localStorage.getItem('showContactToggle') === 'true'; + showPoiToggle = localStorage.getItem('showPoiToggle') === 'true'; + + showContactToggleCheckbox.checked = showContactToggle; + showPoiToggleCheckbox.checked = showPoiToggle; + + updateToggleVisibility(); +} + +// Save settings to localStorage +function saveSettings() { + localStorage.setItem('showContactToggle', showContactToggle.toString()); + localStorage.setItem('showPoiToggle', showPoiToggle.toString()); +} + +// Update toggle button visibility based on settings +function updateToggleVisibility() { + toggleBtn.style.display = showContactToggle ? 'block' : 'none'; + poiToggleBtn.style.display = showPoiToggle ? 'block' : 'none'; +} + +function initOverlay() { + contactOverlay.style.display = 'none'; + poiOverlay.style.display = 'none'; + settingsOverlay.style.display = 'none'; + toggleBtn.textContent = '👤'; + + // Load settings and update visibility + loadSettings(); + console.log(tracks); + + toggleBtn.addEventListener('click', function () { + contactOverlayVisible = !contactOverlayVisible; + if (contactOverlayVisible) { + poiOverlayVisible = false; + settingsOverlayVisible = false; + } + showHideOverlays(); + }); + + poiToggleBtn.addEventListener('click', function () { + poiOverlayVisible = !poiOverlayVisible; + if (poiOverlayVisible) { + contactOverlayVisible = false; + settingsOverlayVisible = false; + } + showHideOverlays(); + }); + + settingsBtn.addEventListener('click', function () { + settingsOverlayVisible = !settingsOverlayVisible; + if (settingsOverlayVisible) { + contactOverlayVisible = false; + poiOverlayVisible = false; + } + showHideOverlays(); + }); + + // Settings checkbox event listeners + showContactToggleCheckbox.addEventListener('change', function () { + showContactToggle = this.checked; + saveSettings(); + updateToggleVisibility(); + }); + + showPoiToggleCheckbox.addEventListener('change', function () { + showPoiToggle = this.checked; + saveSettings(); + updateToggleVisibility(); + }); +} + +// Global function to show/hide overlays +function showHideOverlays() { + contactOverlay.style.display = contactOverlayVisible ? 'block' : 'none'; + poiOverlay.style.display = poiOverlayVisible ? 'block' : 'none'; + settingsOverlay.style.display = settingsOverlayVisible ? 'block' : 'none'; +} + +// Update the contacts overlay +function updateContactsOverlay() { + const contactsList = document.getElementById('contactsList'); + + if (contactsData.size === 0) { + contactsList.innerHTML = + '
No contacts shared their location yet
'; + // Only hide contact toggle if setting is disabled, otherwise respect the setting + if (!showContactToggle) { + toggleBtn.style.display = 'none'; + } + return; + } + + if (contactsData.size > 1) { + toggleBtn.textContent = '👥'; + } + + // Show contact toggle button if there are contacts AND setting is enabled + toggleBtn.style.display = showContactToggle ? 'block' : 'none'; + + let html = ''; + contactsData.forEach((contact, contactId) => { + const timeAgo = formatTimeAgo(contact.lastTimestamp); + html += ` +
+
+
${htmlentities(contact.name)}
+
${timeAgo}
+ +
+ `; + }); + + contactsList.innerHTML = html; +} + +// Update the POI overlay +function updatePoiOverlay() { + const poiList = document.getElementById('poiList'); + + if (poiData.size === 0) { + poiList.innerHTML = '
No POIs yet
'; + // Only hide POI toggle if setting is disabled, otherwise respect the setting + if (!showPoiToggle) { + poiToggleBtn.style.display = 'none'; + } + return; + } + + // Show POI toggle button if there are POIs AND setting is enabled + poiToggleBtn.style.display = showPoiToggle ? 'block' : 'none'; + + let html = ''; + poiData.forEach((poi, poiId) => { + const timeAgo = formatTimeAgo(poi.timestamp); + html += ` +
+
+
${htmlentities( + poi.label || poi.name + )}
+
${timeAgo}
+ +
+ `; + }); + + poiList.innerHTML = html; +} + +// Format timestamp to relative time (e.g., "2h ago", "30m ago", "3d ago") +function formatTimeAgo(timestamp) { + if (!timestamp) return ''; + + const now = Math.floor(Date.now() / 1000); + const diff = now - timestamp; + + if (diff < 60) { + return 'now'; + } else if (diff < 3600) { + const minutes = Math.floor(diff / 60); + return minutes + 'm ago'; + } else if (diff < 86400) { + const hours = Math.floor(diff / 3600); + return hours + 'h ago'; + } else { + const days = Math.floor(diff / 86400); + return days + 'd ago'; + } +} + +// Function to zoom to a specific contact's last position +function zoomToContact(contactId) { + const contact = contactsData.get(contactId); + if (contact && contact.lastPosition) { + zoomToPosition(contact.lastPosition); + } else { + console.log('Contact not found or no position'); + } +} + +// Function to zoom to a specific POI +function zoomToPoi(poiId) { + console.log('poiData contents:', poiData); + const poi = poiData.get(poiId); + console.log('Found poi:', poi); + if (poi && poi.position) { + zoomToPosition(poi.position); + } else { + console.log('POI not found or no position'); + } +} + +function zoomToPosition(position) { + map.setView(position, 15, { animate: true, duration: 1.2 }); +} +// Initialize overlay when DOM is ready +document.addEventListener('DOMContentLoaded', function () { + initOverlay(); + updateContactsOverlay(); + updatePoiOverlay(); +}); +const tracks = {}; +let initDone = false; + +/** + * @type {Payload} + * Example payload: + * { + * action: "pos", + * lat: 47.994828, + * lng: 7.849881, + * timestamp: 1712928222, + * contactId: 123, // can be used as a unique ID to differ tracks etc + * name: "Alice", + * color: "#ff8080", + * independent: false, // false: current or past position of contact, true: a POI + * label: "" // used for POI only + * } + */ +window.webxdc + .setUpdateListener((update) => { + const payload = update.payload; + + if (payload.action === 'pos') { + if (payload.independent) { + // Store POI data for overlay + const poiId = + 'poi_' + + Date.now() + + '_' + + Math.random().toString(36).substr(2, 9); + const poiDataObj = { + name: payload.name, + label: payload.label, + color: payload.color, + position: [payload.lat, payload.lng], + timestamp: payload.timestamp, + }; + console.log('Adding POI with ID:', poiId, 'Data:', poiDataObj); + poiData.set(poiId, poiDataObj); -// set up webxdc + // Update POI overlay + updatePoiOverlay(); -window.webxdc.setUpdateListener((update) => { - const payload = update.payload; - if (payload.action === 'pos') { - if (payload.independent) { - var marker = L.marker([payload.lat, payload.lng], { - icon: pinIcon + const marker = L.marker([payload.lat, payload.lng], { + icon: pinIcon, }).addTo(map); - if (payload.label) { - marker.bindTooltip(shortLabelHtml(payload.label), { - permanent: true, - interactive: true, - direction: 'bottom', - offset: [0, -17], - className: 'poi-tooltip' - }).openTooltip(); - } - marker.on('click', function () { - if (!marker.getPopup()) { - marker.bindPopup(popupHtml(payload), { closeButton: false }).openPopup(); + if (payload.label) { + marker + .bindTooltip(shortLabelHtml(payload.label), { + permanent: true, + interactive: true, + direction: 'bottom', + offset: [0, -17], + className: 'poi-tooltip', + }) + .openTooltip(); } - }); - } else { - if (!tracks[payload.contactId]) { - tracks[payload.contactId] = { - lines: [[]], - payload: payload, - lastTimestamp: payload.timestamp, - marker: null, - polyline: null - }; + marker.on('click', function () { + if (!marker.getPopup()) { + marker + .bindPopup(popupHtml(payload), { + closeButton: false, + }) + .openPopup(); + } + }); } else { - tracks[payload.contactId].payload = payload; - } + // Update contacts data for overlay + if (!contactsData.has(payload.contactId)) { + contactsData.set(payload.contactId, { + name: payload.name, + color: payload.color, + lastPosition: [payload.lat, payload.lng], + lastTimestamp: payload.timestamp, + }); + } else { + const contact = contactsData.get(payload.contactId); + contact.name = payload.name; + contact.color = payload.color; + contact.lastPosition = [payload.lat, payload.lng]; + contact.lastTimestamp = payload.timestamp; + } - var lastLine = tracks[payload.contactId].lines.length - 1; - if ((payload.timestamp - tracks[payload.contactId].lastTimestamp) > 5 * 60) { - // larger time difference: start new line and connect with previous point on track - if (tracks[payload.contactId].lines[lastLine].length == 1) { - tracks[payload.contactId].lines[lastLine].push(tracks[payload.contactId].lines[lastLine][0]); + // Update overlay + updateContactsOverlay(); + + if (!tracks[payload.contactId]) { + tracks[payload.contactId] = { + lines: [[]], + payload: payload, + lastTimestamp: payload.timestamp, + marker: null, + polyline: null, + }; + } else { + tracks[payload.contactId].payload = payload; } - tracks[payload.contactId].lines.push([]); - lastLine++; - } - tracks[payload.contactId].lines[lastLine].push([payload.lat, payload.lng]); - tracks[payload.contactId].lastTimestamp = payload.timestamp; - if (initDone) { - updateTrack(payload.contactId); + let lastLine = tracks[payload.contactId].lines.length - 1; + if ( + payload.timestamp - + tracks[payload.contactId].lastTimestamp > + 5 * 60 + ) { + // larger time difference: start new line and connect with previous point on track + if (tracks[payload.contactId].lines[lastLine].length == 1) { + tracks[payload.contactId].lines[lastLine].push( + tracks[payload.contactId].lines[lastLine][0] + ); + } + tracks[payload.contactId].lines.push([]); + lastLine++; + } + + tracks[payload.contactId].lines[lastLine].push([ + payload.lat, + payload.lng, + ]); + tracks[payload.contactId].lastTimestamp = payload.timestamp; + if (initDone) { + updateTrack(payload.contactId); + } } } - } -}).then(() => { - updateTracks(); - initDone = true; -}); - - + }) + .then(() => { + updateTracks(); + initDone = true; + }); // contact's tracks function updateTrack(contactId) { - var track = tracks[contactId]; + const track = tracks[contactId]; if (track.polyline) { map.removeLayer(track.polyline); } - track.polyline = L.polyline(track.lines, {color: track.payload.color, weight: 4}).addTo(map); - - var content = '' + shortLabelHtml(track.payload.name) + ''; + track.polyline = L.polyline(track.lines, { + color: track.payload.color, + weight: 4, + }).addTo(map); + + const content = + '' + + shortLabelHtml(track.payload.name) + + ""; const age = Math.floor(Date.now() / 1000) - track.payload.timestamp; - if (age > 60*60) { - content += '
' + Math.floor(age/60/60) + 'h ago'; - } else if (age > 30*60) { + if (age > 60 * 60) { + content += + '
' + + Math.floor(age / 60 / 60) + + "h ago"; + } else if (age > 30 * 60) { content += '
½h ago'; - } else if (age > 15*60) { + } else if (age > 15 * 60) { content += '
¼h ago'; } else { content += '
'; } const lastLine = track.lines.length - 1; - const lastLatLng = track.lines[lastLine][ track.lines[lastLine].length-1 ]; + const lastLatLng = track.lines[lastLine][track.lines[lastLine].length - 1]; if (track.marker) { map.removeLayer(track.marker); } track.marker = L.marker(lastLatLng, { - icon: pinIcon, - opacity: 0 - }).addTo(map); - var tooltip = L.tooltip({ - content: content, - permanent: true, - interactive: true, - direction: 'bottom', - offset: [0, -28], - className: 'ppl-tooltip' - }); + icon: pinIcon, + opacity: 0, + }).addTo(map); + const tooltip = L.tooltip({ + content: content, + permanent: true, + interactive: true, + direction: 'bottom', + offset: [0, -28], + className: 'ppl-tooltip', + }); track.marker.bindTooltip(tooltip).openTooltip(); track.marker.unbindPopup(); track.marker.on('click', function () { if (!track.marker.getPopup()) { - track.marker.bindPopup(popupHtml(track.payload), { closeButton: false }).openPopup(); + track.marker + .bindPopup(popupHtml(track.payload), { closeButton: false }) + .openPopup(); } }); } @@ -151,20 +524,20 @@ function updateTracks() { setInterval(() => { updateTracks(); // update is needed for the relative time shown -}, 60*1000); - +}, 60 * 1000); // share a dedicated location -var popup; -var popupLatlng; +let popup; +let popupLatlng; function onSend() { const elem = document.getElementById('textToSend'); - const value = elem.value.trim(); - if (value != "") { + const value = elem.value.trim(); + if (value != '') { popup.close(); - webxdc.sendUpdate({ + webxdc.sendUpdate( + { payload: { action: 'pos', independent: true, @@ -173,26 +546,34 @@ function onSend() { lng: popupLatlng.lng, label: elem.value, name: webxdc.selfName, - color: '#888' + color: '#888', }, - }, 'POI added to map at ' + popupLatlng.lat.toFixed(4) + '/' + popupLatlng.lng.toFixed(4) + ' with text: ' + value); + }, + 'POI added to map at ' + + popupLatlng.lat.toFixed(4) + + '/' + + popupLatlng.lng.toFixed(4) + + ' with text: ' + + value + ); } else { - elem.placeholder = elem.placeholder == 'Label' ? "Enter label" : "Label"; // just some cheap visual feedback + elem.placeholder = + elem.placeholder == 'Label' ? 'Enter label' : 'Label'; // just some cheap visual feedback } } function onMapLongClick(e) { popupLatlng = e.latlng; - popup = L.popup({closeButton: false, keepInView: true}) + popup = L.popup({ closeButton: false, keepInView: true }) .setLatLng(popupLatlng) - .setContent('


') + .setContent( + '


' + ) .openOn(map); } map.on('contextmenu', onMapLongClick); - - // handle position and zoom function onMapMoveOrZoom(e) { @@ -204,29 +585,49 @@ function onMapMoveOrZoom(e) { map.on('moveend', onMapMoveOrZoom); map.on('zoomend', onMapMoveOrZoom); - +// Close overlays when clicking on the map +map.on('click', function () { + contactOverlayVisible = false; + poiOverlayVisible = false; + settingsOverlayVisible = false; + showHideOverlays(); +}); // tools function htmlentities(rawStr) { - return rawStr.replace(/[\u00A0-\u9999<>\&]/g, ((i) => `&#${i.charCodeAt(0)};`)); + return rawStr.replace( + /[\u00A0-\u9999<>\&]/g, + (i) => `&#${i.charCodeAt(0)};` + ); } function shortLabelHtml(label) { if (label.length > 9) { - label = htmlentities(label.substring(0, 8).trim()) + ".."; + label = htmlentities(label.substring(0, 8).trim()) + '..'; } else if (label.length <= 4) { - const padding = ' '.repeat((7-label.length)/2); + const padding = ' '.repeat((7 - label.length) / 2); label = padding + htmlentities(label) + padding; } return label; } function popupHtml(payload) { - return '
' + htmlentities(payload.name) + '
' - + '
' + htmlentities(payload.label) + '
' - + '
' - + payload.lat.toFixed(4) + '°/' + payload.lng.toFixed(4) + '°
' - + htmlentities(new Date(payload.timestamp*1000).toLocaleString()) - + '
'; + return ( + '
' + + htmlentities(payload.name) + + '
' + + '
' + + htmlentities(payload.label) + + '
' + + '
' + + payload.lat.toFixed(4) + + '°/' + + payload.lng.toFixed(4) + + '°
' + + htmlentities(new Date(payload.timestamp * 1000).toLocaleString()) + + '
' + ); } diff --git a/mapServices.js b/mapServices.js new file mode 100644 index 0000000..1266620 --- /dev/null +++ b/mapServices.js @@ -0,0 +1,42 @@ +const defaultServiceKey = 'OpenStreetMap'; +const mapServices = { + 'OpenStreetMap': { + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { + maxZoom: 19, + attribution: '© OpenStreetMap' + } + }, + 'OSmap.de': { + url: 'https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c', 'd'], + options: { + maxZoom: 18, + attribution: '© OSmap.de' + } + }, + 'OSmap.fr': { + url: 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + options: { + maxZoom: 18, + attribution: '© OSmap.fr' + } + }, + 'opentopomap': { + url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + options: { + maxZoom: 18, + attribution: '© opentopomap' + } + }, + '高德地图': { + url: 'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', + subdomains: '1234', + options: { + maxZoom: 18, + attribution: '© 高德地图' + } + } +}; diff --git a/style.css b/style.css new file mode 100644 index 0000000..2fc917f --- /dev/null +++ b/style.css @@ -0,0 +1,397 @@ +body { + padding: 0; + margin: 0; +} + +html, body, #map { + height: 100%; + width: 100vw; +} + +#map { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.poi-tooltip { + font-weight: bold; + font-size: 15px; + color: #ff3b30; + text-shadow: 1px 1px 1px #fff, -1px -1px 1px #fff, 1px -1px 1px #fff, -1px 1px 1px #fff; + background: transparent; + box-shadow: none; + border: none; +} +.ppl-tooltip { + text-align: right; + font-size: 15px; + color: white; + text-shadow: none; + background: transparent; + box-shadow: none; + border: none; +} +.ppl-name { + padding: 0 5px; + font-weight: bold; + border: 1px solid white; + border-radius: 10px; +} +.ppl-time { + text-shadow: 1px 1px 1px #fff, -1px -1px 1px #fff, 1px -1px 1px #fff, -1px 1px 1px #fff !important; + position: relative; + color: #333; + font-size: 12px; + top: -7px; +} +.ppl-online { + height: 10px; width: 10px; + border: 1px solid white; + border-radius: 50%; + display: inline-block; + position: relative; right: -4px; top: -14px; + background-color: #34c759; +} +.poi-tooltip::before, .ppl-tooltip::before { + border: none; +} + +.leaflet-control-scale-line { + text-shadow: 1px 1px 1px #fff, -1px -1px 1px #fff, 1px -1px 1px #fff, -1px 1px 1px #fff; + background: rgba(255, 255, 255, 0.4); +} + +.leaflet-control-attribution { + text-shadow: 1px 1px 1px #fff, -1px -1px 1px #fff, 1px -1px 1px #fff, -1px 1px 1px #fff !important; + background: transparent !important; + padding: 2px 6px; +} + +.leaflet-top { + top: 45%; + background: transparent !important; +} +.leaflet-control-zoom { + border: 0 !important; +} +.leaflet-control-zoom-in, .leaflet-control-zoom-out { + color: #333 !important; + font-size: 30px !important; +} +.leaflet-bar a { + border-radius: 50% !important; + background: rgba(255, 255, 255, 0.8); + padding: 10px; + margin: 10px; +} + +.formx { + text-align: center; +} +.formx input, .formx button { + text-align: center; + font-size: 16px; + border: 1px solid #BBB; + border-radius: 8px; +} +.formx button { + margin-top: 4px; + + background-color: transparent; +} + + +/*** overlays ***/ + +.overlay { + display: none; + position: absolute; + top: 20px; + left: 80px; + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + padding: 15px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.55); + max-width: 300px; + max-height: 400px; + overflow-y: auto; + z-index: 1000; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.overlay h3 { + margin: 0 0 15px 0; + font-size: 16px; + color: #333; + text-align: center; + border-bottom: 2px solid #007AFF; + padding-bottom: 8px; +} + +.contact-item { + display: flex; + align-items: center; + margin-bottom: 5px; + padding: 8px; + cursor: pointer; + border-radius: 24px; +} + +.contact-item:hover { + background: rgba(104, 98, 98, 0.1); +} + +.contact-color { + width: 16px; + height: 16px; + border-radius: 50%; + margin-right: 10px; + border: 2px solid #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + margin-left: 6px; +} + +.contact-name { + flex: 1; + font-size: 14px; + color: #333; + font-weight: 500; +} + +.contact-time { + font-size: 12px; + color: #666; + margin: 0 8px; + white-space: nowrap; +} + +.item-button { + background: #60a1574a; + color: white; + border: none; + border-radius: 50%; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s; + margin-left: 5px; + height: 32px; + width: 32px; +} + +.item-button:hover { + background: #60a1578b; +} + +.no-items { + text-align: center; + color: #666; + font-style: italic; + padding: 20px; +} + +.toggle-overlay { + position: absolute; + top: 20px; + left: 40px; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + -webkit-border-radius: 50%; + width: 50px; + height: 50px; + min-width: 50px; + min-height: 50px; + max-width: 50px; + max-height: 50px; + font-size: 18px; + cursor: pointer; + z-index: 1001; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); + transition: all 0.2s; + /* Ensure proper centering and circular shape in webviews */ + box-sizing: border-box; + -webkit-box-sizing: border-box; + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; + /* Force hardware acceleration for better rendering */ + -webkit-transform: translateX(-50%) translateZ(0); + transform: translateX(-50%) translateZ(0); + /* Center the emoji content */ + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + /* Prevent text wrapping and ensure circular shape */ + white-space: nowrap; + overflow: hidden; +} + +.toggle-overlay:hover { + background: rgba(255, 255, 255, 1); + transform: scale(1.05); + -webkit-transform: translateX(-50%) scale(1.05) translateZ(0); +} + +/* Additional webview compatibility for Android (to make icons look the same)*/ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-overlay { + /* Force subpixel rendering */ + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + /* Ensure proper touch handling */ + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + /* High-DPI display compatibility */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Ensure consistent sizing */ + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + } +} + +/* Additional media queries for different screen densities */ +@media screen and (-webkit-min-device-pixel-ratio: 2) { + .toggle-overlay { + /* Ensure crisp rendering on high-DPI displays */ + image-rendering: -webkit-optimize-contrast; + image-rendering: optimize-contrast; + } +} + +.overlay-hidden .overlay { + display: none; +} + +/* POI overlay specific styles */ +.poi-toggle { + top: 80px; + left: 40px; +} + +.poi-toggle:hover { + -webkit-transform: translateX(-50%) scale(1.05) translateZ(0); + transform: translateX(-50%) scale(1.05) translateZ(0); +} + +.poi-overlay { + top: 80px; +} + +.toggle-button:hover { + background: #f0f0f0; +} + +.toggle-button.active { + background: #007bff; + border-color: #0056b3; + color: white; +} + +.toggle-button svg { + width: 24px; + height: 24px; + /* Ensure icon is perfectly centered */ + display: block; + margin: auto; +} + +/* Settings button */ +.settings-button { + position: absolute; + top: 20px; + right: 20px; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + font-size: 18px; + cursor: pointer; + z-index: 1001; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + white-space: nowrap; + overflow: hidden; +} + +.settings-button:hover { + background: rgba(255, 255, 255, 1); + transform: scale(1.05); +} + +.settings-button img { + width: 24px; + height: 24px; + display: block; + margin: auto; +} + +/* Settings overlay */ +.settings-overlay { + top: 20px; + right: 90px; + left: auto; + max-width: 280px; +} + +.settings-content { + display: flex; + flex-direction: column; + gap: 15px; +} + +.setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid #eee; +} + +.setting-item:last-child { + border-bottom: none; +} + +.setting-item label { + font-size: 14px; + color: #333; + font-weight: 500; + cursor: pointer; + flex: 1; + padding-right: 5px; +} + +.setting-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #007AFF; +} + +.setting-item select { + width: 120px; + padding: 4px 8px; + font-size: 12px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: white; + cursor: pointer; +} + +.setting-item select:focus { + border-color: #007AFF; + outline: none; +}