diff --git a/index.html b/index.html index b1a01e1..798787b 100644 --- a/index.html +++ b/index.html @@ -115,11 +115,144 @@ background-color: transparent; } + + /* overlay */ + .overlay { + display: none; + position: absolute; + top: 20px; + left: 70px; + 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; + } + + .contact-color { + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 10px; + border: 2px solid #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + } + + .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: 20px; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 18px; + cursor: pointer; + z-index: 1001; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); + transition: all 0.2s; + } + + .toggle-overlay:hover { + background: rgba(255, 255, 255, 1); + transform: scale(1.05); + } + + .overlay-hidden .overlay { + display: none; + } + + /* POI overlay specific styles */ + .poi-toggle { + top: 70px; + } + + .poi-overlay { + top: 70px; + } +
+ + + + + +
+

Contacts

+
+
No contacts yet
+
+
+ +
+

Points of Interest

+
+
No POIs yet
+
+
diff --git a/index.js b/index.js index 0357499..95dc15a 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ // set up map -var map = L.map('map', { +const map = L.map('map', { doubleClickZoom: true, zoomControl: false, // added manually below tapHold: true @@ -15,12 +15,164 @@ map.attributionControl.setPrefix(''); L.control.scale({position: 'bottomleft'}).addTo(map); L.control.zoom({position: 'topright'}).addTo(map); +// Overlay management +let contactOverlayVisible = false; +let poiOverlayVisible = false; +const contactsData = new Map(); // Store contact data for the overlay +const poiData = new Map(); // Store POI data for the overlay +// DOM elements +const contactOverlay = document.getElementById('contactsOverlay'); +const poiOverlay = document.getElementById('poiOverlay'); +const toggleBtn = document.getElementById('toggleOverlay'); +const poiToggleBtn = document.getElementById('togglePoiOverlay'); + +function initOverlay() { + contactOverlay.style.display = 'none'; + poiOverlay.style.display = 'none'; + toggleBtn.textContent ='👤'; + poiToggleBtn.style.display = 'none'; // Hidden by default + console.log(tracks); + + toggleBtn.addEventListener('click', function() { + contactOverlayVisible = !contactOverlayVisible; + if (contactOverlayVisible) { + poiOverlayVisible = false; + } + showHideOverlays(); + }); + + poiToggleBtn.addEventListener('click', function() { + poiOverlayVisible = !poiOverlayVisible; + if (poiOverlayVisible) { + contactOverlayVisible = false; + } + showHideOverlays(); + }); + + function showHideOverlays() { + contactOverlay.style.display = contactOverlayVisible ? 'block' : 'none'; + poiOverlay.style.display = poiOverlayVisible ? '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
'; + return; + } + + if (contactsData.size > 1) { + toggleBtn.textContent = '👥'; + } + + 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
'; + poiToggleBtn.style.display = 'none'; + return; + } + + // Show POI toggle button if there are POIs + poiToggleBtn.style.display = 'block'; + + 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(); +}); + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(map); -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 @@ -28,18 +180,45 @@ var pinIcon = L.icon({ popupAnchor: [0, -29] // point from which the popup should open relative to the iconAnchor }); -var tracks = {}; -var initDone = false; - - - -// set up webxdc +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) { - var marker = L.marker([payload.lat, payload.lng], { + // 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); + + // Update POI overlay + updatePoiOverlay(); + + const marker = L.marker([payload.lat, payload.lng], { icon: pinIcon }).addTo(map); if (payload.label) { @@ -57,6 +236,25 @@ window.webxdc.setUpdateListener((update) => { } }); } else { + // 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; + } + + // Update overlay + updateContactsOverlay(); + if (!tracks[payload.contactId]) { tracks[payload.contactId] = { lines: [[]], @@ -69,7 +267,7 @@ window.webxdc.setUpdateListener((update) => { tracks[payload.contactId].payload = payload; } - var lastLine = tracks[payload.contactId].lines.length - 1; + const 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) { @@ -96,14 +294,14 @@ window.webxdc.setUpdateListener((update) => { // 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) + ''; + let 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'; @@ -117,6 +315,14 @@ function updateTrack(contactId) { const lastLine = track.lines.length - 1; const lastLatLng = track.lines[lastLine][ track.lines[lastLine].length-1 ]; + + // Update contacts data with latest position + if (contactsData.has(contactId)) { + const contact = contactsData.get(contactId); + contact.lastPosition = lastLatLng; + contact.lastTimestamp = track.lastTimestamp; + } + if (track.marker) { map.removeLayer(track.marker); } @@ -124,7 +330,7 @@ function updateTrack(contactId) { icon: pinIcon, opacity: 0 }).addTo(map); - var tooltip = L.tooltip({ + const tooltip = L.tooltip({ content: content, permanent: true, interactive: true, @@ -145,6 +351,9 @@ function updateTracks() { for (contactId in tracks) { updateTrack(contactId); } + // Update overlays after updating all tracks + updateContactsOverlay(); + updatePoiOverlay(); } setInterval(() => { @@ -154,8 +363,8 @@ setInterval(() => { // share a dedicated location -var popup; -var popupLatlng; +const popup = null; +const popupLatlng = null; function onSend() { const elem = document.getElementById('textToSend'); diff --git a/manifest.toml b/manifest.toml index 5d74bf3..81a7c4e 100644 --- a/manifest.toml +++ b/manifest.toml @@ -1,3 +1,4 @@ name = "Maps" source_code_url = "https://github.com/deltachat/maps" request_internet_access = true +request_integration = 'map'