Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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));
}

Expand All @@ -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() {
}
}
2 changes: 2 additions & 0 deletions src/map-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
153 changes: 153 additions & 0 deletions src/map/controls/geolocate-control.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="6" fill="var(--interactive-accent)" stroke="white" stroke-width="2"/>
</svg>
`;
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;
}
}