diff --git a/.gitignore b/.gitignore index 97f3520..0a31a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.xdc - +node_modules* +dist/* diff --git a/README.md b/README.md index cdee2f2..3535675 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,17 @@ it is not bound to Leaflet. ## Building +first time run: + +```sh +npm install +``` +After changes run + +```sh +npm run build +``` + to create `maps.xdc` file, execute: ```sh diff --git a/create-xdc.sh b/create-xdc.sh index f06d683..c36e6bd 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" * -x "node_modules/*" @ --exclude index.ts LICENSE README.md webxdc.js webxdc.d.ts icon.png "*screenshot*" "*marker-*png" "*-src*" "*.map" "*.sh" "*.xdc" *.DS_Store package.json package-lock.json tsconfig.json types.d.ts echo "success, archive contents:" unzip -l "$PACKAGE_NAME.xdc" diff --git a/index.html b/index.html index 798787b..2e6118c 100644 --- a/index.html +++ b/index.html @@ -253,7 +253,7 @@

Points of Interest

No POIs yet
- + diff --git a/index.js b/index.ts similarity index 68% rename from index.js rename to index.ts index 95dc15a..8f101c1 100644 --- a/index.js +++ b/index.ts @@ -1,16 +1,26 @@ +// Reference type definitions +/// +/// + // set up map +const map: L.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')); + const lat = localStorage.getItem('map.lat'); + const lng = localStorage.getItem('map.lng'); + const zoom = localStorage.getItem('map.zoom'); + if (lat && lng && zoom) { + map.setView([parseFloat(lat), parseFloat(lng)], parseInt(zoom)); + } } + map.attributionControl.setPrefix(''); L.control.scale({position: 'bottomleft'}).addTo(map); L.control.zoom({position: 'topright'}).addTo(map); @@ -18,18 +28,19 @@ 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 +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'); +const contactOverlay = document.getElementById('contactsOverlay') as HTMLElement; +const poiOverlay = document.getElementById('poiOverlay') as HTMLElement; +const toggleBtn = document.getElementById('toggleOverlay') as HTMLButtonElement; +const poiToggleBtn = document.getElementById('togglePoiOverlay') as HTMLButtonElement; -function initOverlay() { +function initOverlay(): void { contactOverlay.style.display = 'none'; poiOverlay.style.display = 'none'; - toggleBtn.textContent ='👤'; + toggleBtn.textContent = '👤'; poiToggleBtn.style.display = 'none'; // Hidden by default console.log(tracks); @@ -49,15 +60,15 @@ function initOverlay() { showHideOverlays(); }); - function showHideOverlays() { + function showHideOverlays(): void { contactOverlay.style.display = contactOverlayVisible ? 'block' : 'none'; poiOverlay.style.display = poiOverlayVisible ? 'block' : 'none'; } } // Update the contacts overlay -function updateContactsOverlay() { - const contactsList = document.getElementById('contactsList'); +function updateContactsOverlay(): void { + const contactsList = document.getElementById('contactsList') as HTMLElement; if (contactsData.size === 0) { contactsList.innerHTML = '
No contacts shared their location yet
'; @@ -85,8 +96,8 @@ function updateContactsOverlay() { } // Update the POI overlay -function updatePoiOverlay() { - const poiList = document.getElementById('poiList'); +function updatePoiOverlay(): void { + const poiList = document.getElementById('poiList') as HTMLElement; if (poiData.size === 0) { poiList.innerHTML = '
No POIs yet
'; @@ -114,7 +125,7 @@ function updatePoiOverlay() { } // Format timestamp to relative time (e.g., "2h ago", "30m ago", "3d ago") -function formatTimeAgo(timestamp) { +function formatTimeAgo(timestamp: number): string { if (!timestamp) return ''; const now = Math.floor(Date.now() / 1000); @@ -135,7 +146,7 @@ function formatTimeAgo(timestamp) { } // Function to zoom to a specific contact's last position -function zoomToContact(contactId) { +function zoomToContact(contactId: number): void { const contact = contactsData.get(contactId); if (contact && contact.lastPosition) { zoomToPosition(contact.lastPosition); @@ -145,7 +156,7 @@ function zoomToContact(contactId) { } // Function to zoom to a specific POI -function zoomToPoi(poiId) { +function zoomToPoi(poiId: string): void { console.log('poiData contents:', poiData); const poi = poiData.get(poiId); console.log('Found poi:', poi); @@ -156,7 +167,7 @@ function zoomToPoi(poiId) { } } -function zoomToPosition(position) { +function zoomToPosition(position: [number, number]): void { map.setView(position, 15, {animate: true, duration: 1.2}); } @@ -168,44 +179,28 @@ document.addEventListener('DOMContentLoaded', function() { }); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: "© OpenStreetMap" - }).addTo(map); + maxZoom: 19, + attribution: "© OpenStreetMap" +}).addTo(map); 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 }); - -const tracks = {}; +const tracks: 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) => { +window.webxdc.setUpdateListener((update: { payload: Payload }) => { 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 = { + const poiDataObj: PoiData = { name: payload.name, label: payload.label, color: payload.color, @@ -219,17 +214,19 @@ window.webxdc.setUpdateListener((update) => { updatePoiOverlay(); const marker = L.marker([payload.lat, payload.lng], { - icon: pinIcon - }).addTo(map); + 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(); + 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(); @@ -246,10 +243,12 @@ window.webxdc.setUpdateListener((update) => { }); } 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; + if (contact) { + contact.name = payload.name; + contact.color = payload.color; + contact.lastPosition = [payload.lat, payload.lng]; + contact.lastTimestamp = payload.timestamp; + } } // Update overlay @@ -274,7 +273,10 @@ window.webxdc.setUpdateListener((update) => { tracks[payload.contactId].lines[lastLine].push(tracks[payload.contactId].lines[lastLine][0]); } tracks[payload.contactId].lines.push([]); - lastLine++; + const newLastLine = lastLine + 1; + if (initDone) { + updateTrack(payload.contactId); + } } tracks[payload.contactId].lines[lastLine].push([payload.lat, payload.lng]); @@ -289,11 +291,8 @@ window.webxdc.setUpdateListener((update) => { initDone = true; }); - - // contact's tracks - -function updateTrack(contactId) { +function updateTrack(contactId: number): void { const track = tracks[contactId]; if (track.polyline) { @@ -319,25 +318,29 @@ function updateTrack(contactId) { // Update contacts data with latest position if (contactsData.has(contactId)) { const contact = contactsData.get(contactId); - contact.lastPosition = lastLatLng; - contact.lastTimestamp = track.lastTimestamp; + if (contact) { + contact.lastPosition = lastLatLng; + contact.lastTimestamp = track.lastTimestamp; + } } if (track.marker) { map.removeLayer(track.marker); } track.marker = L.marker(lastLatLng, { - icon: pinIcon, - opacity: 0 - }).addTo(map); + icon: pinIcon, + opacity: 0 + }).addTo(map); + const tooltip = L.tooltip({ - content: content, - permanent: true, - interactive: true, - direction: 'bottom', - offset: [0, -28], - className: 'ppl-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 () { @@ -347,9 +350,9 @@ function updateTrack(contactId) { }); } -function updateTracks() { - for (contactId in tracks) { - updateTrack(contactId); +function updateTracks(): void { + for (const contactId in tracks) { + updateTrack(parseInt(contactId)); } // Update overlays after updating all tracks updateContactsOverlay(); @@ -360,66 +363,63 @@ setInterval(() => { updateTracks(); // update is needed for the relative time shown }, 60*1000); - // share a dedicated location +let popup: L.Popup | null = null; +let popupLatlng: L.LatLng | null = null; -const popup = null; -const popupLatlng = null; - -function onSend() { - const elem = document.getElementById('textToSend'); - const value = elem.value.trim(); - if (value != "") { +function onSend(): void { + const elem = document.getElementById('textToSend') as HTMLInputElement; + const value = elem.value.trim(); + if (value != "" && popup && popupLatlng) { popup.close(); - webxdc.sendUpdate({ - payload: { - action: 'pos', - independent: true, - timestamp: Math.floor(Date.now() / 1000), - lat: popupLatlng.lat, - lng: popupLatlng.lng, - label: elem.value, - name: webxdc.selfName, - color: '#888' - }, - }, 'POI added to map at ' + popupLatlng.lat.toFixed(4) + '/' + popupLatlng.lng.toFixed(4) + ' with text: ' + value); + window.webxdc.sendUpdate({ + payload: { + action: 'pos', + independent: true, + timestamp: Math.floor(Date.now() / 1000), + lat: popupLatlng.lat, + lng: popupLatlng.lng, + label: elem.value, + name: window.webxdc.selfName, + color: '#888', + contactId: 0 // Required by interface but not used for POIs + }, + }, '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 + if (elem.placeholder === 'Label') { + elem.placeholder = "Enter label"; + } else { + elem.placeholder = "Label"; + } // just some cheap visual feedback } } -function onMapLongClick(e) { +function onMapLongClick(e: L.LeafletMouseEvent): void { popupLatlng = e.latlng; popup = L.popup({closeButton: false, keepInView: true}) .setLatLng(popupLatlng) - .setContent('


') + .setContent('


') .openOn(map); } map.on('contextmenu', onMapLongClick); - - // handle position and zoom - -function onMapMoveOrZoom(e) { - localStorage.setItem('map.lat', map.getCenter().lat); - localStorage.setItem('map.lng', map.getCenter().lng); - localStorage.setItem('map.zoom', map.getZoom()); +function onMapMoveOrZoom(e: L.LeafletEvent): void { + localStorage.setItem('map.lat', map.getCenter().lat.toString()); + localStorage.setItem('map.lng', map.getCenter().lng.toString()); + localStorage.setItem('map.zoom', map.getZoom().toString()); } map.on('moveend', onMapMoveOrZoom); map.on('zoomend', onMapMoveOrZoom); - - // tools - -function htmlentities(rawStr) { +function htmlentities(rawStr: string): string { return rawStr.replace(/[\u00A0-\u9999<>\&]/g, ((i) => `&#${i.charCodeAt(0)};`)); } -function shortLabelHtml(label) { +function shortLabelHtml(label: string): string { if (label.length > 9) { label = htmlentities(label.substring(0, 8).trim()) + ".."; } else if (label.length <= 4) { @@ -429,11 +429,16 @@ function shortLabelHtml(label) { return label; } -function popupHtml(payload) { +function popupHtml(payload: Payload): string { return '
' + htmlentities(payload.name) + '
' - + '
' + htmlentities(payload.label) + '
' + + '
' + htmlentities(payload.label || '') + '
' + '
' + payload.lat.toFixed(4) + '°/' + payload.lng.toFixed(4) + '°
' + htmlentities(new Date(payload.timestamp*1000).toLocaleString()) + '
'; } + +// Make functions globally available for onclick handlers +(window as any).zoomToContact = zoomToContact; +(window as any).zoomToPoi = zoomToPoi; +(window as any).onSend = onSend; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f38f5a6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "maps-webxdc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maps-webxdc", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.16", + "webxdc-types": "^1.0.1" + }, + "devDependencies": { + "@types/leaflet": "^1.9.20", + "typescript": "^5.0.0" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.20", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz", + "integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/webxdc-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webxdc-types/-/webxdc-types-1.0.1.tgz", + "integrity": "sha512-voocssWnUiMoJoicPWmCKnjd6OG8TNuUq+5n9+0gMSBMg0h1KxapnN9Vq6WssK0jhKcGBSZvcQf8tGK+X88IaQ==", + "license": "unlicense" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..688dbcd --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "maps-webxdc", + "version": "1.0.0", + "description": "A webxdc maps application with TypeScript", + "main": "index.ts", + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && tsc", + "watch": "tsc --watch", + "dev": "npm run build && python3 -m http.server 8000" + }, + "devDependencies": { + "@types/leaflet": "^1.9.20", + "typescript": "^5.0.0" + }, + "keywords": [ + "webxdc", + "maps", + "typescript", + "leaflet" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.16", + "webxdc-types": "^1.0.1" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3ff0797 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,51 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/geojson': + specifier: ^7946.0.16 + version: 7946.0.16 + webxdc-types: + specifier: ^1.0.1 + version: 1.0.1 + devDependencies: + '@types/leaflet': + specifier: ^1.9.20 + version: 1.9.20 + typescript: + specifier: ^5.0.0 + version: 5.9.2 + +packages: + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/leaflet@1.9.20': + resolution: {integrity: sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + webxdc-types@1.0.1: + resolution: {integrity: sha512-voocssWnUiMoJoicPWmCKnjd6OG8TNuUq+5n9+0gMSBMg0h1KxapnN9Vq6WssK0jhKcGBSZvcQf8tGK+X88IaQ==} + +snapshots: + + '@types/geojson@7946.0.16': {} + + '@types/leaflet@1.9.20': + dependencies: + '@types/geojson': 7946.0.16 + + typescript@5.9.2: {} + + webxdc-types@1.0.1: {} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a506bd8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": true, + "outDir": "./dist", + "rootDir": "./", + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "isolatedModules": false, + "noEmit": false, + "declaration": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "noImplicitAny": false + }, + "include": [ + "*.ts", + "types.d.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..7b0cab1 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,40 @@ +// Application-specific type definitions (not library types) + +interface Payload { + action: string; + lat: number; + lng: number; + timestamp: number; + contactId: number; + name: string; + color: string; + independent: boolean; + label?: string; +} + +interface ContactData { + name: string; + color: string; + lastPosition: [number, number]; + lastTimestamp: number; +} + +interface PoiData { + name: string; + label?: string; + color: string; + position: [number, number]; + timestamp: number; +} + +interface Track { + lines: [number, number][][]; + payload: Payload; + lastTimestamp: number; + marker: L.Marker | null; + polyline: L.Polyline | null; +} + +interface Tracks { + [contactId: number]: Track; +}