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() { } } 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..4a7d828 --- /dev/null +++ b/src/map/controls/geolocate-control.ts @@ -0,0 +1,153 @@ +import { setIcon, Notice } from 'obsidian'; +import { Map, Marker } from 'maplibre-gl'; + +export class CustomGeolocateControl { + private containerEl: HTMLElement; + 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'); + } + + onAdd(map: Map): HTMLElement { + this.map = map; + + // Create the locate button + this.locateButton = this.containerEl.createEl('div', { + cls: 'maplibregl-ctrl-geolocate canvas-control-item', + attr: { 'aria-label': 'Locate user' } + }); + 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; + } + + 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; + + if (this.locateButton) { + this.locateButton.removeClass('is-active'); + } + + if (this.userMarker) { + this.userMarker.remove(); + this.userMarker = null; + } + } + + private updatePosition(lat: number, lng: number): void { + if (!this.map) return; + + // 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]); + } + + // Fly to user location + this.map.flyTo({ + center: [lng, lat], + zoom: Math.max(this.map.getZoom(), 15), + duration: 1000 + }); + } + + 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 { + this.stopTracking(); + + if (this.containerEl && this.containerEl.parentNode) { + this.containerEl.detach(); + } + + this.map = null; + this.locateButton = null; + } +}