From 83e5506114134e8254876bc394a4f015efb9a491 Mon Sep 17 00:00:00 2001 From: Steph Ango <10565871+kepano@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:20:26 -0500 Subject: [PATCH 1/4] Locate --- src/map-view.ts | 2 + src/map/controls/geolocate-control.ts | 71 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/map/controls/geolocate-control.ts diff --git a/src/map-view.ts b/src/map-view.ts index 5dedcf6..357da4d 100644 --- a/src/map-view.ts +++ b/src/map-view.ts @@ -13,6 +13,7 @@ import { LngLatLike, Map, setRTLTextPlugin } from 'maplibre-gl'; import type ObsidianMapsPlugin from './main'; import { DEFAULT_MAP_HEIGHT, DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from './map/constants'; import { CustomZoomControl } from './map/controls/zoom-control'; +import { CustomGeolocateControl } from './map/controls/geolocate-control'; import { BackgroundSwitcherControl } from './map/controls/background-switcher'; import { StyleManager } from './map/style'; import { PopupManager } from './map/popup'; @@ -209,6 +210,7 @@ export class MapView extends BasesView { this.markerManager.setMap(this.map); this.map.addControl(new CustomZoomControl(), 'top-right'); + this.map.addControl(new CustomGeolocateControl(), 'top-right'); // Add background switcher if multiple tile sets are available if (this.plugin.settings.tileSets.length > 1) { diff --git a/src/map/controls/geolocate-control.ts b/src/map/controls/geolocate-control.ts new file mode 100644 index 0000000..b54571d --- /dev/null +++ b/src/map/controls/geolocate-control.ts @@ -0,0 +1,71 @@ +import { setIcon } from 'obsidian'; +import { Map, GeolocateControl as MapLibreGeolocateControl } from 'maplibre-gl'; + +export class CustomGeolocateControl { + private containerEl: HTMLElement; + private geolocateControl: MapLibreGeolocateControl; + private map: Map | null = null; + + constructor() { + this.containerEl = createDiv('maplibregl-ctrl maplibregl-ctrl-group canvas-control-group mod-raised'); + + // Create the underlying MapLibre GeolocateControl + this.geolocateControl = new MapLibreGeolocateControl({ + positionOptions: { + enableHighAccuracy: true + }, + trackUserLocation: true + }); + } + + onAdd(map: Map): HTMLElement { + this.map = map; + + // Create the locate button + const locateButton = this.containerEl.createEl('div', { + cls: 'maplibregl-ctrl-geolocate canvas-control-item', + attr: { 'aria-label': 'Locate user' } + }); + setIcon(locateButton, 'locate-fixed'); + + // Add the MapLibre geolocate control to the map (hidden) + // This handles all the geolocation logic + map.addControl(this.geolocateControl, 'top-right'); + + // Hide the default geolocate control UI + const defaultControl = map.getContainer().querySelector('.maplibregl-ctrl-geolocate'); + if (defaultControl && defaultControl.parentElement) { + defaultControl.parentElement.style.display = 'none'; + } + + // Trigger geolocation when our custom button is clicked + locateButton.addEventListener('click', () => { + this.geolocateControl.trigger(); + }); + + // Update button appearance based on geolocation state + this.geolocateControl.on('geolocate', () => { + locateButton.addClass('is-active'); + }); + + this.geolocateControl.on('trackuserlocationend', () => { + locateButton.removeClass('is-active'); + }); + + this.geolocateControl.on('error', (error) => { + console.warn('Geolocation error:', error); + locateButton.removeClass('is-active'); + }); + + return this.containerEl; + } + + onRemove(): void { + if (this.map) { + this.map.removeControl(this.geolocateControl); + } + if (this.containerEl && this.containerEl.parentNode) { + this.containerEl.detach(); + } + } +} From 09bda3b3c51db04c4d50843e3b582f38bda52206 Mon Sep 17 00:00:00 2001 From: Steph Ango <10565871+kepano@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:39:26 -0500 Subject: [PATCH 2/4] Command to get coordinates --- src/main.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 1511b23..acd48b4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { Plugin } from 'obsidian'; +import { Plugin, Notice } from 'obsidian'; import { MapView } from './map-view'; import { MapSettings, DEFAULT_SETTINGS, MapSettingTab } from './settings'; @@ -15,6 +15,14 @@ export default class ObsidianMapsPlugin extends Plugin { options: MapView.getViewOptions, }); + this.addCommand({ + id: 'copy-current-location', + name: 'Copy current location to clipboard', + callback: () => { + this.getCurrentLocationAndCopy(); + } + }); + this.addSettingTab(new MapSettingTab(this.app, this)); } @@ -26,6 +34,53 @@ export default class ObsidianMapsPlugin extends Plugin { await this.saveData(this.settings); } + private getCurrentLocationAndCopy(): void { + if (!navigator.geolocation) { + new Notice('Geolocation is not supported by your browser'); + return; + } + + new Notice('Getting your location...'); + + navigator.geolocation.getCurrentPosition( + (position) => { + const lat = Math.round(position.coords.latitude * 100000) / 100000; + const lng = Math.round(position.coords.longitude * 100000) / 100000; + const coordString = `[${lat}, ${lng}]`; + + navigator.clipboard.writeText(coordString).then(() => { + new Notice(`Location copied: ${coordString}`); + }).catch((error) => { + console.error('Failed to copy to clipboard:', error); + new Notice('Failed to copy to clipboard'); + }); + }, + (error) => { + console.error('Geolocation error:', error); + let errorMessage = 'Failed to get location'; + + switch (error.code) { + case error.PERMISSION_DENIED: + errorMessage = 'Location permission denied'; + break; + case error.POSITION_UNAVAILABLE: + errorMessage = 'Location information unavailable'; + break; + case error.TIMEOUT: + errorMessage = 'Location request timed out'; + break; + } + + new Notice(errorMessage); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0 + } + ); + } + onunload() { } } From dc7c2e2d5d017f0170a23c08e0f3fc59e4961e5f Mon Sep 17 00:00:00 2001 From: Steph Ango <10565871+kepano@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:41:46 -0500 Subject: [PATCH 3/4] Clean up --- src/map/controls/geolocate-control.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/map/controls/geolocate-control.ts b/src/map/controls/geolocate-control.ts index b54571d..9cbde27 100644 --- a/src/map/controls/geolocate-control.ts +++ b/src/map/controls/geolocate-control.ts @@ -8,8 +8,7 @@ export class CustomGeolocateControl { constructor() { this.containerEl = createDiv('maplibregl-ctrl maplibregl-ctrl-group canvas-control-group mod-raised'); - - // Create the underlying MapLibre GeolocateControl + this.geolocateControl = new MapLibreGeolocateControl({ positionOptions: { enableHighAccuracy: true @@ -20,16 +19,13 @@ export class CustomGeolocateControl { onAdd(map: Map): HTMLElement { this.map = map; - - // Create the locate button + const locateButton = this.containerEl.createEl('div', { cls: 'maplibregl-ctrl-geolocate canvas-control-item', attr: { 'aria-label': 'Locate user' } }); setIcon(locateButton, 'locate-fixed'); - // Add the MapLibre geolocate control to the map (hidden) - // This handles all the geolocation logic map.addControl(this.geolocateControl, 'top-right'); // Hide the default geolocate control UI @@ -38,7 +34,6 @@ export class CustomGeolocateControl { defaultControl.parentElement.style.display = 'none'; } - // Trigger geolocation when our custom button is clicked locateButton.addEventListener('click', () => { this.geolocateControl.trigger(); }); From 656b3daf5b9ce8f9cd9fbf83aa2282e6fa564fc0 Mon Sep 17 00:00:00 2001 From: Steph Ango <10565871+kepano@users.noreply.github.com> Date: Mon, 22 Dec 2025 08:05:56 +0800 Subject: [PATCH 4/4] Rewrite the geolocate control to better handle permissions --- src/map/controls/geolocate-control.ts | 157 ++++++++++++++++++++------ 1 file changed, 122 insertions(+), 35 deletions(-) diff --git a/src/map/controls/geolocate-control.ts b/src/map/controls/geolocate-control.ts index 9cbde27..4a7d828 100644 --- a/src/map/controls/geolocate-control.ts +++ b/src/map/controls/geolocate-control.ts @@ -1,66 +1,153 @@ -import { setIcon } from 'obsidian'; -import { Map, GeolocateControl as MapLibreGeolocateControl } from 'maplibre-gl'; +import { setIcon, Notice } from 'obsidian'; +import { Map, Marker } from 'maplibre-gl'; export class CustomGeolocateControl { private containerEl: HTMLElement; - private geolocateControl: MapLibreGeolocateControl; + private locateButton: HTMLElement | null = null; private map: Map | null = null; + private userMarker: Marker | null = null; + private watchId: number | null = null; + private isTracking = false; constructor() { this.containerEl = createDiv('maplibregl-ctrl maplibregl-ctrl-group canvas-control-group mod-raised'); - - this.geolocateControl = new MapLibreGeolocateControl({ - positionOptions: { - enableHighAccuracy: true - }, - trackUserLocation: true - }); } onAdd(map: Map): HTMLElement { this.map = map; - - const locateButton = this.containerEl.createEl('div', { + + // Create the locate button + this.locateButton = this.containerEl.createEl('div', { cls: 'maplibregl-ctrl-geolocate canvas-control-item', attr: { 'aria-label': 'Locate user' } }); - setIcon(locateButton, 'locate-fixed'); + setIcon(this.locateButton, 'locate-fixed'); + + // Trigger geolocation when button is clicked + this.locateButton.addEventListener('click', () => { + if (this.isTracking) { + this.stopTracking(); + } else { + this.startTracking(); + } + }); + + return this.containerEl; + } - map.addControl(this.geolocateControl, 'top-right'); + private startTracking(): void { + if (!navigator.geolocation) { + new Notice('Geolocation is not supported by your browser'); + return; + } + + if (!this.map || !this.locateButton) return; + + this.isTracking = true; + this.locateButton.addClass('is-active'); + + // Get initial position and fly to it + navigator.geolocation.getCurrentPosition( + (position) => { + this.updatePosition(position.coords.latitude, position.coords.longitude); + }, + (error) => { + this.handleError(error); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0 + } + ); + + // Watch for position changes + this.watchId = navigator.geolocation.watchPosition( + (position) => { + this.updatePosition(position.coords.latitude, position.coords.longitude); + }, + (error) => { + this.handleError(error); + }, + { + enableHighAccuracy: true, + maximumAge: 0 + } + ); + } + + private stopTracking(): void { + if (this.watchId !== null) { + navigator.geolocation.clearWatch(this.watchId); + this.watchId = null; + } + + this.isTracking = false; - // Hide the default geolocate control UI - const defaultControl = map.getContainer().querySelector('.maplibregl-ctrl-geolocate'); - if (defaultControl && defaultControl.parentElement) { - defaultControl.parentElement.style.display = 'none'; + if (this.locateButton) { + this.locateButton.removeClass('is-active'); } - locateButton.addEventListener('click', () => { - this.geolocateControl.trigger(); - }); + if (this.userMarker) { + this.userMarker.remove(); + this.userMarker = null; + } + } - // Update button appearance based on geolocation state - this.geolocateControl.on('geolocate', () => { - locateButton.addClass('is-active'); - }); + private updatePosition(lat: number, lng: number): void { + if (!this.map) return; - this.geolocateControl.on('trackuserlocationend', () => { - locateButton.removeClass('is-active'); - }); + // Create or update user marker + if (!this.userMarker) { + const el = createDiv('user-location-marker'); + el.innerHTML = ` + + + + `; + this.userMarker = new Marker({ element: el }) + .setLngLat([lng, lat]) + .addTo(this.map); + } else { + this.userMarker.setLngLat([lng, lat]); + } - this.geolocateControl.on('error', (error) => { - console.warn('Geolocation error:', error); - locateButton.removeClass('is-active'); + // Fly to user location + this.map.flyTo({ + center: [lng, lat], + zoom: Math.max(this.map.getZoom(), 15), + duration: 1000 }); + } - return this.containerEl; + private handleError(error: GeolocationPositionError): void { + let errorMessage = 'Failed to get location'; + + switch (error.code) { + case error.PERMISSION_DENIED: + errorMessage = 'Location permission denied'; + break; + case error.POSITION_UNAVAILABLE: + errorMessage = 'Location information unavailable'; + break; + case error.TIMEOUT: + errorMessage = 'Location request timed out'; + break; + } + + new Notice(errorMessage); + console.warn('Geolocation error:', error); + this.stopTracking(); } onRemove(): void { - if (this.map) { - this.map.removeControl(this.geolocateControl); - } + this.stopTracking(); + if (this.containerEl && this.containerEl.parentNode) { this.containerEl.detach(); } + + this.map = null; + this.locateButton = null; } }