From 4d452ea1fddb9c08e0100054d9700b010cf12698 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 18:04:00 -0500 Subject: [PATCH 01/20] feat: copy coordinates context menu --- src/map-renderer.ts | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 423e818..16b03fd 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -1,4 +1,4 @@ -import { App, TFile, setIcon } from 'obsidian'; +import { App, TFile, setIcon, Menu } from 'obsidian'; import { Deck, PickingInfo, MapViewState, FlyToInterpolator, Layer } from '@deck.gl/core'; import { BitmapLayer, IconLayer, ScatterplotLayer, PolygonLayer } from '@deck.gl/layers'; import { TileLayer } from '@deck.gl/geo-layers'; @@ -162,13 +162,18 @@ function getIconSVG(iconName: string, strokeWidth: number, fill: boolean): strin function handlePointClick(info: PickingInfo, event: MjolnirEvent, options: MapRendererOptions['options'], app: App) { if (!info.object) return; + // Ignore right-clicks (button === 2) + const srcEvent = event?.srcEvent; + if (srcEvent && 'button' in srcEvent && srcEvent.button === 2) { + return; + } + if (options.onMarkerClick) { options.onMarkerClick(info.object.point, event); return; } if (info.object.point.file) { - const srcEvent = event?.srcEvent; const newTab = (srcEvent && 'button' in srcEvent && srcEvent.button === 1) || srcEvent?.ctrlKey || @@ -703,5 +708,36 @@ export function createMapRenderer(config: MapRendererOptions): Deck { + e.preventDefault(); + e.stopPropagation(); + + const rect = mapCanvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // @ts-expect-error - accessing internal viewState + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const viewState = deck.viewState?.MapView; + + if (viewState) { + const viewport = deck.getViewports()[0]; + const [lng, lat] = viewport?.unproject([x, y]) || [0, 0]; + + const menu = new Menu(); + + menu.addItem((item) => { + item + .setTitle('Copy coordinates') + .setIcon('copy') + .onClick(async () => { + await navigator.clipboard.writeText(`${lat.toFixed(6)}, ${lng.toFixed(6)}`); + }); + }); + + menu.showAtMouseEvent(e); + } + }); + return deck; } From c9c6cf77ae266ddf8cfb9307f0ef88a120d65dcd Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 18:09:08 -0500 Subject: [PATCH 02/20] feat: 'open in web' and 'set as center' --- src/map-renderer.ts | 21 +++++++++++++++++++++ src/views/map-bases-view.ts | 3 +++ 2 files changed, 24 insertions(+) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 16b03fd..c4f37fb 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -67,6 +67,7 @@ export interface MapRendererOptions { autoCenter?: boolean; onMarkerClick?: (point: MapPoint, event: MjolnirEvent) => void; onTilesLoaded?: () => void; + onSetMapCenter?: (lat: number, lng: number) => void; }; } @@ -735,6 +736,26 @@ export function createMapRenderer(config: MapRendererOptions): Deck { + item + .setTitle('Set as map center') + .setIcon('map-pin') + .onClick(() => { + options.onSetMapCenter?.(lat, lng); + }); + }); + } + + menu.addItem((item) => { + item + .setTitle('Open in web') + .setIcon('globe') + .onClick(() => { + window.open(`https://www.google.com/maps?q=${lat.toFixed(6)},${lng.toFixed(6)}`, '_blank'); + }); + }); + menu.showAtMouseEvent(e); } }); diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 386c7e5..7d5409b 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -287,6 +287,9 @@ export class MapBasesView extends BasesView { onTilesLoaded: () => { tilesLoaded = true; hideOverlay(); + }, + onSetMapCenter: (lat: number, lng: number) => { + this.config.set('center', `${lat}, ${lng}`); } } }); From 750725ee4eb1d6e80ffd75a1c02614f78910e81a Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 18:15:46 -0500 Subject: [PATCH 03/20] feat: add location search bar via photon api --- src/views/map-bases-view.ts | 168 +++++++++++++++++++++++++++++++++++- styles.css | 58 +++++++++++++ 2 files changed, 225 insertions(+), 1 deletion(-) diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 7d5409b..9f8ee89 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -7,8 +7,9 @@ import { QueryController, StringValue, ViewOption, + requestUrl, } from 'obsidian'; -import { Deck } from '@deck.gl/core'; +import { Deck, FlyToInterpolator } from '@deck.gl/core'; import { MapView as MapViewType } from '@deck.gl/core'; import { createMapRenderer, MapPoint, updateMapPoints } from '../map-renderer'; import MapPlugin from '../main'; @@ -298,6 +299,8 @@ export class MapBasesView extends BasesView { this.containerEl.removeClass('is-loading'); + this.createSearchBox(); + setTimeout(() => { if (!tilesLoaded) { hideOverlay(); @@ -576,6 +579,169 @@ export class MapBasesView extends BasesView { return null; } + private createSearchBox(): void { + interface PhotonFeature { + geometry: { + coordinates: [number, number]; + }; + properties: { + name?: string; + street?: string; + city?: string; + country?: string; + }; + } + + interface PhotonResponse { + features: PhotonFeature[]; + } + + const searchContainer = this.mapEl.createDiv({ cls: 'bases-map-search-container' }); + this.makeDraggable(searchContainer); + + const searchInput = searchContainer.createEl('input', { + type: 'text', + cls: 'map-search-input', + placeholder: 'Search location...', + }); + + const resultsContainer = searchContainer.createDiv({ cls: 'map-search-results' }); + + let searchTimeout: number; + let currentResults: Array<{ lat: number; lng: number; name: string; display_name: string }> = []; + + const performSearch = async (query: string) => { + try { + // Using Photon API (free, no API key, OSM-based) + const response = await requestUrl(`https://photon.komoot.io/api/?q=${encodeURIComponent(query)}&limit=5`); + const data = response.json as PhotonResponse; + + currentResults = data.features.map((feature: PhotonFeature) => ({ + lat: feature.geometry.coordinates[1], + lng: feature.geometry.coordinates[0], + name: feature.properties.name ?? feature.properties.street ?? '', + display_name: [ + feature.properties.name, + feature.properties.city, + feature.properties.country + ].filter(Boolean).join(', ') + })); + + resultsContainer.empty(); + + if (currentResults.length > 0) { + currentResults.forEach((result) => { + const resultItem = resultsContainer.createDiv({ cls: 'map-search-result-item' }); + resultItem.textContent = result.display_name; + + resultItem.addEventListener('click', () => { + if (this.deck) { + // Center map on selected location + this.deck.setProps({ + initialViewState: { + MapView: { + latitude: result.lat, + longitude: result.lng, + zoom: 12, + transitionDuration: 800, + transitionInterpolator: new FlyToInterpolator(), + } + } + }); + } + searchInput.value = result.name || result.display_name; + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + }); + }); + resultsContainer.addClass('visible'); + } else { + resultsContainer.removeClass('visible'); + } + } catch (error) { + console.error('Search error:', error); + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + } + }; + + searchInput.addEventListener('input', () => { + const query = searchInput.value.trim(); + + if (searchTimeout) { + window.clearTimeout(searchTimeout); + } + + if (query.length < 3) { + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + return; + } + + searchTimeout = window.setTimeout(() => { + void performSearch(query); + }, 300); + }); + + // Close results when clicking outside + document.addEventListener('click', (e) => { + if (!searchContainer.contains(e.target as Node)) { + resultsContainer.removeClass('visible'); + } + }); + } + + private makeDraggable(element: HTMLElement): void { + let isDragging = false; + let offsetX = 0; + let offsetY = 0; + + element.addClass('map-draggable'); + + const onMouseDown = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'SELECT' || target.tagName === 'BUTTON') { + return; + } + + isDragging = true; + const rect = element.getBoundingClientRect(); + + offsetX = e.clientX - rect.left; + offsetY = e.clientY - rect.top; + + element.addClass('dragging'); + + e.preventDefault(); + }; + + const onMouseMove = (e: MouseEvent) => { + if (!isDragging) return; + + const containerRect = this.mapEl.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + + let x = e.clientX - containerRect.left - offsetX; + let y = e.clientY - containerRect.top - offsetY; + + // Constrain to container bounds + x = Math.max(0, Math.min(x, containerRect.width - elementRect.width)); + y = Math.max(0, Math.min(y, containerRect.height - elementRect.height)); + + element.setCssProps({'left': `${x}px`}); + element.setCssProps({'top': `${y}px`}); + element.addClass('move'); + }; + + const onMouseUp = () => { + isDragging = false; + }; + + element.addEventListener('mousedown', onMouseDown); + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + } + static getViewOptions(): ViewOption[] { return [ { diff --git a/styles.css b/styles.css index 5fb9e35..746ead2 100644 --- a/styles.css +++ b/styles.css @@ -566,4 +566,62 @@ color: var(--text-muted); font-style: italic; font-size: 0.9em; +} + +/* Map search box */ +.bases-map-search-container { + position: absolute; + top: 8px; + left: 8px; + width: 280px; + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + padding: 8px; +} + +.map-search-input { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background: var(--background-secondary); + color: var(--text-normal); + font-size: 13px; +} + +.map-search-input:focus { + outline: none; + border-color: var(--interactive-accent); +} + +.map-search-results { + display: none; + margin-top: 4px; + max-height: 200px; + overflow-y: auto; + border-radius: 6px; + background: var(--background-secondary); +} + +.map-search-results.visible { + display: block; +} + +.map-search-result-item { + padding: 8px 12px; + cursor: pointer; + font-size: 12px; + color: var(--text-normal); + border-bottom: 1px solid var(--background-modifier-border); +} + +.map-search-result-item:last-child { + border-bottom: none; +} + +.map-search-result-item:hover { + background: var(--background-modifier-hover); } \ No newline at end of file From 335c39d1b739ba75611256748ed98858017d216f Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 18:15:51 -0500 Subject: [PATCH 04/20] feat: fadein search results --- styles.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/styles.css b/styles.css index 746ead2..12d1352 100644 --- a/styles.css +++ b/styles.css @@ -598,16 +598,21 @@ } .map-search-results { - display: none; margin-top: 4px; max-height: 200px; overflow-y: auto; border-radius: 6px; background: var(--background-secondary); + opacity: 0; + transform: translateY(-8px); + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; } .map-search-results.visible { - display: block; + opacity: 1; + transform: translateY(0); + pointer-events: all; } .map-search-result-item { From 0c129c7fa19d7d5903f81d8851191e8167503a08 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 18:26:01 -0500 Subject: [PATCH 05/20] feat: default zoom context menu item --- src/map-renderer.ts | 14 +++++++ src/views/map-bases-view.ts | 5 ++- src/views/map-timeline-bases-view.ts | 57 ---------------------------- 3 files changed, 18 insertions(+), 58 deletions(-) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index c4f37fb..137f412 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -68,6 +68,7 @@ export interface MapRendererOptions { onMarkerClick?: (point: MapPoint, event: MjolnirEvent) => void; onTilesLoaded?: () => void; onSetMapCenter?: (lat: number, lng: number) => void; + onSetDefaultZoom?: (zoom: number) => void; }; } @@ -747,6 +748,19 @@ export function createMapRenderer(config: MapRendererOptions): Deck { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const currentZoom = (viewState as any).zoom as number; + item + .setTitle('Set default zoom') + .setIcon('zoom-in') + .onClick(() => { + options.onSetDefaultZoom?.(currentZoom); + }); + }); + } + menu.addItem((item) => { item .setTitle('Open in web') diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 9f8ee89..fb2d649 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -291,6 +291,9 @@ export class MapBasesView extends BasesView { }, onSetMapCenter: (lat: number, lng: number) => { this.config.set('center', `${lat}, ${lng}`); + }, + onSetDefaultZoom: (zoom: number) => { + this.config.set('defaultZoom', zoom); } } }); @@ -691,7 +694,7 @@ export class MapBasesView extends BasesView { }); } - private makeDraggable(element: HTMLElement): void { + protected makeDraggable(element: HTMLElement): void { let isDragging = false; let offsetX = 0; let offsetY = 0; diff --git a/src/views/map-timeline-bases-view.ts b/src/views/map-timeline-bases-view.ts index 1c9729f..19bb0fa 100644 --- a/src/views/map-timeline-bases-view.ts +++ b/src/views/map-timeline-bases-view.ts @@ -316,63 +316,6 @@ export class MapTimelineBasesView extends MapBasesView { } } - private makeDraggable(element: HTMLElement): void { - let isDragging = false; - let offsetX = 0; - let offsetY = 0; - - element.addClass('map-draggable'); - - const onMouseDown = (e: MouseEvent) => { - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'SELECT' || target.tagName === 'BUTTON') { - return; - } - - isDragging = true; - const rect = element.getBoundingClientRect(); - - offsetX = e.clientX - rect.left; - offsetY = e.clientY - rect.top; - - element.addClass('dragging'); - - e.preventDefault(); - }; - - const onMouseMove = (e: MouseEvent) => { - if (!isDragging) return; - - const containerRect = this.mapEl.getBoundingClientRect(); - const elementRect = element.getBoundingClientRect(); - - let x = e.clientX - containerRect.left - offsetX; - let y = e.clientY - containerRect.top - offsetY; - - // Constrain to container bounds - x = Math.max(0, Math.min(x, containerRect.width - elementRect.width)); - y = Math.max(0, Math.min(y, containerRect.height - elementRect.height)); - - element.setCssProps({'left': `${x}px`}); - element.setCssProps({'top': `${y}px`}); - element.addClass('move'); - }; - - const onMouseUp = () => { - isDragging = false; - }; - - element.addEventListener('mousedown', onMouseDown); - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - - // Clean up on destroy - this.register(() => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }); - } - private togglePlayback(): void { if (this.isPlaying) { this.stopPlayback(); From c241449435553b35509db4cab5c14929b787de22 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 18:34:49 -0500 Subject: [PATCH 06/20] fix: set initial config state and remove point equality check to `updateMap` --- src/views/map-bases-view.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index fb2d649..53f34cd 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -299,6 +299,7 @@ export class MapBasesView extends BasesView { }); this.lastPoints = points; + this.lastConfigState = { ...this.config.data }; this.containerEl.removeClass('is-loading'); @@ -327,11 +328,6 @@ export class MapBasesView extends BasesView { const points = this.extractPointsFromData(); - if (arePointsEqual(points, this.lastPoints)) { - console.warn('onDataUpdated triggered but points are unchanged - skipping update'); - return; - } - this.updateRenderedPoints(points); } From cbe1021b27054b4e9df2e6545b57d04962d510a7 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 22:29:04 -0500 Subject: [PATCH 07/20] refactor: clean up types/eslint --- src/map-renderer.ts | 7 ++----- src/pointutils.ts | 13 +------------ src/views/map-bases-view.ts | 12 ++++-------- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 137f412..b3a3e21 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -718,9 +718,7 @@ export function createMapRenderer(config: MapRendererOptions): Deck { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const currentZoom = (viewState as any).zoom as number; + const currentZoom = viewState.zoom; item .setTitle('Set default zoom') .setIcon('zoom-in') diff --git a/src/pointutils.ts b/src/pointutils.ts index 1bc44e3..8dc091c 100644 --- a/src/pointutils.ts +++ b/src/pointutils.ts @@ -1,18 +1,7 @@ import { MapPoint } from "./map-renderer"; - export function haveLocationsChanged(points1: MapPoint[], points2: MapPoint[]): boolean { - // Check if count changed - if (points1.length !== points2.length) return true; - - // Check if any location (lat/lng) changed - for (let i = 0; i < points1.length; i++) { - if (points1[i].lat !== points2[i].lat || points1[i].lng !== points2[i].lng) { - return true; - } - } - - return false; + return !arePointsEqual(points1, points2); } export function arePointsEqual(points1: MapPoint[], points2: MapPoint[]): boolean { diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 53f34cd..99d52b1 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -9,11 +9,11 @@ import { ViewOption, requestUrl, } from 'obsidian'; -import { Deck, FlyToInterpolator } from '@deck.gl/core'; +import { Deck, FlyToInterpolator, MapViewState } from '@deck.gl/core'; import { MapView as MapViewType } from '@deck.gl/core'; import { createMapRenderer, MapPoint, updateMapPoints } from '../map-renderer'; import MapPlugin from '../main'; -import { arePointsEqual, haveLocationsChanged } from '../pointutils'; +import { haveLocationsChanged } from '../pointutils'; export const MapBasesViewType = 'map'; @@ -29,7 +29,7 @@ export class MapBasesView extends BasesView { protected deck: Deck | null = null; - protected savedViewState: { latitude: number; longitude: number; zoom: number } | null = null; + protected savedViewState: MapViewState | null = null; protected lastPoints: MapPoint[] = []; protected mapUpdateTimeout?: number; protected lastConfigState: Record = {}; @@ -75,11 +75,8 @@ export class MapBasesView extends BasesView { private destroyMap(): void { if (this.deck) { try { - // @ts-expect-error - accessing protected viewState - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const viewState = this.deck.viewState?.MapView; + const viewState = this.deck.props.viewState?.MapView; if (viewState) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.savedViewState = viewState; } this.deck.finalize(); @@ -635,7 +632,6 @@ export class MapBasesView extends BasesView { resultItem.addEventListener('click', () => { if (this.deck) { - // Center map on selected location this.deck.setProps({ initialViewState: { MapView: { From 3a3940ddd53e3694c5b99b5a1e8ad9e41da8831a Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sat, 22 Nov 2025 22:37:34 -0500 Subject: [PATCH 08/20] refactor: clean up types/eslint --- src/main.ts | 3 +-- src/map-renderer.ts | 4 ++-- src/thumbnail-cache.ts | 3 +-- src/views/map-bases-view.ts | 11 +++-------- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4b98d55..ae41f7a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,8 +41,7 @@ export default class MapPlugin extends Plugin { } async loadSettings() { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as MapPluginSettings); } async saveSettings() { diff --git a/src/map-renderer.ts b/src/map-renderer.ts index b3a3e21..d8dcb6d 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -465,6 +465,7 @@ export function createMapRenderer(config: MapRendererOptions): Deck; }) => { + //TODO: replace with boundingBox // eslint-disable-next-line @typescript-eslint/no-deprecated const bbox = props.tile.bbox; if (!('west' in bbox)) return null; @@ -497,8 +498,7 @@ export function createMapRenderer(config: MapRendererOptions): Deck(numPoints); for (let i = 0; i < numPoints; i++) { const point = points[i]; diff --git a/src/thumbnail-cache.ts b/src/thumbnail-cache.ts index 252487c..713bf37 100644 --- a/src/thumbnail-cache.ts +++ b/src/thumbnail-cache.ts @@ -44,8 +44,7 @@ export class ThumbnailCacheManager { if (exists) { const content = await this.app.vault.adapter.read(metadataPath); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.cache = JSON.parse(content); + this.cache = JSON.parse(content) as ThumbnailCacheMetadata; } this.cacheLoaded = true; diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 99d52b1..e61a28c 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -129,8 +129,7 @@ export class MapBasesView extends BasesView { if (!boundsStr || typeof boundsStr !== 'string') return undefined; try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsed = JSON.parse(boundsStr); + const parsed = JSON.parse(boundsStr) as unknown[]; if (Array.isArray(parsed) && parsed.length === 2 && Array.isArray(parsed[0]) && parsed[0].length === 2 && Array.isArray(parsed[1]) && parsed[1].length === 2) { @@ -377,9 +376,7 @@ export class MapBasesView extends BasesView { const fileCache = this.app.metadataCache.getFileCache(entry.file); if (fileCache?.frontmatter?.tags) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const tags = fileCache.frontmatter.tags; - point.tags = Array.isArray(tags) ? tags : [tags]; + point.tags = fileCache.frontmatter.tags as string[]; } if (coverProp) { @@ -554,8 +551,7 @@ export class MapBasesView extends BasesView { // Handle string value as JSON array if (value instanceof StringValue) { try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const parsed = JSON.parse(value.toString().trim()); + const parsed = JSON.parse(value.toString().trim()) as unknown[]; if (Array.isArray(parsed)) { const coords: [number, number][] = []; for (const item of parsed) { @@ -608,7 +604,6 @@ export class MapBasesView extends BasesView { const performSearch = async (query: string) => { try { - // Using Photon API (free, no API key, OSM-based) const response = await requestUrl(`https://photon.komoot.io/api/?q=${encodeURIComponent(query)}&limit=5`); const data = response.json as PhotonResponse; From b839de62d245c981a6882059fde42b153f0bc1d3 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 09:10:07 -0500 Subject: [PATCH 09/20] fix: handle autofit for configured center properly --- src/map-renderer.ts | 2 +- src/views/map-bases-view.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index d8dcb6d..110dc31 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -378,7 +378,7 @@ export function updateMapPoints(deck: Deck, points: MapPoint[], c const hasConfiguredCenter = options.center && (options.center[0] !== 0 || options.center[1] !== 0); - if (autoCenter && hasConfiguredCenter && options.zoom) { + if (hasConfiguredCenter && options.zoom) { if (!options.center) return; shouldTransition = true; targetViewState = { diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index e61a28c..5a30453 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -336,11 +336,11 @@ export class MapBasesView extends BasesView { const configChanged = this.hasConfigMeaningfullyChanged(); this.lastConfigState = { ...this.config.data }; - - const shouldAutoCenter = this.plugin.settings.autoCenter && (autofit === true || (autofit === undefined && (locationsChanged || configChanged))); - this.lastPoints = points; + const shouldAutoCenter = this.plugin.settings.autoCenter && !hasConfiguredCenter && (autofit === true || (autofit === undefined && (locationsChanged || configChanged))); + const shouldUseConfiguredCenter = hasConfiguredCenter && (autofit !== false); //TODO: slight hack + updateMapPoints(this.deck, points, { containerEl: this.mapEl, app: this.app, @@ -348,8 +348,8 @@ export class MapBasesView extends BasesView { tagSettings: this.plugin.tagSettings, options: { markerType: this.getMarkerType(), - center: hasConfiguredCenter ? center : undefined, - zoom: hasConfiguredCenter ? this.getDefaultZoom() : undefined, + center: shouldUseConfiguredCenter ? center : undefined, + zoom: shouldUseConfiguredCenter ? this.getDefaultZoom() : undefined, autoCenter: shouldAutoCenter } }); From a8a886c43b843b73bf29170303717ea9d895d340 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 09:24:08 -0500 Subject: [PATCH 10/20] fix: correct viewstate access --- src/map-renderer.ts | 10 +++++++--- src/views/map-bases-view.ts | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 110dc31..ef7e28a 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -16,6 +16,8 @@ function easeCubic(t: number): number { type PropertyValue = string | number | boolean | string[] | null; +export let currentViewState: MapViewState | null = null; + interface MapProperty { name: string; value: PropertyValue; @@ -708,6 +710,9 @@ export function createMapRenderer(config: MapRendererOptions): Deck { + currentViewState = newViewState; + } }); mapCanvas.addEventListener('contextmenu', (e: MouseEvent) => { @@ -717,9 +722,8 @@ export function createMapRenderer(config: MapRendererOptions): Deck Date: Sun, 23 Nov 2025 16:41:13 -0500 Subject: [PATCH 11/20] refactor: `LatLng` type --- src/latlng.ts | 141 ++++++++++++++++++++++++++++++++++++ src/map-renderer.ts | 22 +++--- src/pointutils.ts | 10 +-- src/views/map-bases-view.ts | 51 +++++++------ 4 files changed, 182 insertions(+), 42 deletions(-) create mode 100644 src/latlng.ts diff --git a/src/latlng.ts b/src/latlng.ts new file mode 100644 index 0000000..ceb5c66 --- /dev/null +++ b/src/latlng.ts @@ -0,0 +1,141 @@ +export namespace LatLng { + // Unverified input - accepts various formats + export type Like = + | { lat: number; lng: number } + | { latitude: number; longitude: number } + | [number, number] // [lat, lng] + | string; // "lat,lng" format + + // Verified type using a brand + export type Verified = { + readonly lat: number; + readonly lng: number; + readonly __brand: 'LatLng'; + }; + + // Validation function that narrows the type + export function parse(input: Like): Verified | null { + let lat: number; + let lng: number; + + if (typeof input === 'string') { + const parts = input.split(',').map(s => parseFloat(s.trim())); + if (parts.length !== 2) return null; + [lat, lng] = parts; + } else if (Array.isArray(input)) { + [lat, lng] = input; + } else if ('lat' in input && 'lng' in input) { + lat = input.lat; + lng = input.lng; + } else if ('latitude' in input && 'longitude' in input) { + lat = input.latitude; + lng = input.longitude; + } else { + return null; + } + + // Validation + if ( + typeof lat !== 'number' || + typeof lng !== 'number' || + !isFinite(lat) || + !isFinite(lng) || + lat < -90 || + lat > 90 || + lng < -180 || + lng > 180 + ) { + return null; + } + + // Return branded type + return { lat, lng, __brand: 'LatLng' } as Verified; + } + + // Parse or return default value + export function parseOrDefault(input: Like, defaultValue: Verified): Verified { + return parse(input) ?? defaultValue; + } + + // Parse or return [0, 0] + export function parseOrZero(input: Like): Verified { + return parse(input) ?? ({ lat: 0, lng: 0, __brand: 'LatLng' } as Verified); + } + + // Helper to check if already verified + export function isVerified(input: Like | Verified): input is Verified { + return typeof input === 'object' && !Array.isArray(input) && '__brand' in input; + } + + // Convert to array [lat, lng] + export function toArray(point: Verified): [number, number] { + return [point.lat, point.lng]; + } + + // Convert to array [lng, lat] (for deck.gl) + export function toArrayLngLat(point: Verified): [number, number] { + return [point.lng, point.lat]; + } + + // Convert to object {lat, lng} + export function toObject(point: Verified): { lat: number; lng: number } { + return { lat: point.lat, lng: point.lng }; + } + + // Convert to string "lat, lng" + export function toString(point: Verified, precision = 6): string { + return `${point.lat.toFixed(precision)}, ${point.lng.toFixed(precision)}`; + } + + // Distance calculation (simple Euclidean for now) + export function distance(point1: Verified, point2: Verified): number { + return Math.sqrt(Math.pow(point1.lat - point2.lat, 2) + Math.pow(point1.lng - point2.lng, 2)); + } + + // Haversine distance in kilometers + export function distanceHaversine(point1: Verified, point2: Verified): number { + const R = 6371; // Earth's radius in km + const dLat = toRadians(point2.lat - point1.lat); + const dLng = toRadians(point2.lng - point1.lng); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRadians(point1.lat)) * Math.cos(toRadians(point2.lat)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + // Helper for radians conversion + function toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + // Check if two points are equal + export function equals(point1: Verified, point2: Verified): boolean { + return point1.lat === point2.lat && point1.lng === point2.lng; + } + + // Check if coordinates are valid (without parsing) + export function isValid(lat: number, lng: number): boolean { + return ( + typeof lat === 'number' && + typeof lng === 'number' && + isFinite(lat) && + isFinite(lng) && + lat >= -90 && + lat <= 90 && + lng >= -180 && + lng <= 180 + ); + } + + // Create from lat/lng numbers (with validation) + export function from(lat: number, lng: number): Verified | null { + if (!isValid(lat, lng)) return null; + return { lat, lng, __brand: 'LatLng' } as Verified; + } + + // Create from lat/lng numbers (unsafe, no validation) + export function fromUnsafe(lat: number, lng: number): Verified { + return { lat, lng, __brand: 'LatLng' } as Verified; + } +} diff --git a/src/map-renderer.ts b/src/map-renderer.ts index ef7e28a..7b474cd 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -9,6 +9,7 @@ import { MapView as MapViewType } from '@deck.gl/core'; import { MapTagSettings } from './settings/map-tag-settings'; import type MapPlugin from './main'; import type { ThumbnailCacheManager } from './thumbnail-cache'; +import { LatLng } from './latlng'; function easeCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; @@ -24,8 +25,7 @@ interface MapProperty { } export interface MapPoint { - lat: number; - lng: number; + location: LatLng.Verified; title: string; color?: string; size?: number; @@ -33,7 +33,7 @@ export interface MapPoint { file?: TFile; tags?: string[]; properties?: MapProperty[]; - polygon?: [number, number][]; + polygon?: LatLng.Verified[]; } interface TileIndex { @@ -191,10 +191,10 @@ function calculateBounds(points: MapPoint[], containerEl: HTMLElement): { latitu let minLng = Infinity, maxLng = -Infinity; for (const p of points) { - if (p.lat < minLat) minLat = p.lat; - if (p.lat > maxLat) maxLat = p.lat; - if (p.lng < minLng) minLng = p.lng; - if (p.lng > maxLng) maxLng = p.lng; + if (p.location.lat < minLat) minLat = p.location.lat; + if (p.location.lat > maxLat) maxLat = p.location.lat; + if (p.location.lng < minLng) minLng = p.location.lng; + if (p.location.lng > maxLng) maxLng = p.location.lng; } const centerLat = (maxLat + minLat) / 2; @@ -247,8 +247,8 @@ function createPolygonLayer( for (const point of points) { if (point.polygon && point.polygon.length > 0) { - // Convert lat,lng to lng,lat for deck.gl - const polygon: [number, number][] = point.polygon.map(([lat, lng]) => [lng, lat]); + // Convert LatLng.Verified to [lng, lat] for deck.gl + const polygon: [number, number][] = point.polygon.map(coord => LatLng.toArrayLngLat(coord)); polygonData.push({ polygon, @@ -355,7 +355,7 @@ export function updateMapPoints(deck: Deck, points: MapPoint[], c const autoCenter = options.autoCenter !== false; // Default to true const deckData: DeckDataPoint[] = points.map(point => ({ - position: [point.lng, point.lat] as [number, number], + position: LatLng.toArrayLngLat(point.location), color: parseColor(getPointColor(point, tagSettings, defaultColor)), radius: point.size || markerSize, point: point, @@ -505,7 +505,7 @@ export function createMapRenderer(config: MapRendererOptions): Deck 0) { point.polygon = polygonCoords; } } @@ -432,7 +435,7 @@ export class MapBasesView extends BasesView { return points; } - protected extractCoordinates(entry: BasesEntry, coordinatesProp: BasesPropertyId | null): [number, number] | null { + protected extractCoordinates(entry: BasesEntry, coordinatesProp: BasesPropertyId | null): LatLng.Verified | null { if (coordinatesProp) { try { const value = entry.getValue(coordinatesProp); @@ -455,7 +458,7 @@ export class MapBasesView extends BasesView { const lat = this.parseCoordinate(latValue); const lng = this.parseCoordinate(lngValue); if (lat !== null && lng !== null) { - return [lat, lng]; + return LatLng.from(lat, lng); } } } @@ -502,13 +505,13 @@ export class MapBasesView extends BasesView { return null; } - private parseLatLng(value: unknown): [number, number] | null { + private parseLatLng(value: unknown): LatLng.Verified | null { // Handle ListValue from frontmatter: [40.7128, -74.0060] if (value instanceof ListValue && value.length() >= 2) { const lat = this.parseCoordinate(value.get(0)); const lng = this.parseCoordinate(value.get(1)); if (lat !== null && lng !== null) { - return [lat, lng]; + return LatLng.from(lat, lng); } } @@ -517,7 +520,7 @@ export class MapBasesView extends BasesView { const lat = this.parseCoordinate(value[0]); const lng = this.parseCoordinate(value[1]); if (lat !== null && lng !== null) { - return [lat, lng]; + return LatLng.from(lat, lng); } } @@ -526,22 +529,18 @@ export class MapBasesView extends BasesView { const str = value instanceof StringValue ? value.toString() : value; const parts = str.split(',').map(p => parseFloat(p.trim())); if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { - return [parts[0], parts[1]]; + return LatLng.from(parts[0], parts[1]); } } return null; } - private parseLatLngOrZero(value: unknown): [number, number] { - return this.parseLatLng(value) ?? [0, 0]; - } - - private extractPolygonCoordinates(value: unknown): [number, number][] | null { + private extractPolygonCoordinates(value: unknown): LatLng.Verified[] | null { try { // Handle ListValue (array from frontmatter) if (value instanceof ListValue) { - const coords: [number, number][] = []; + const coords: LatLng.Verified[] = []; for (let i = 0; i < value.length(); i++) { const coord = this.parseLatLng(value.get(i)); if (coord) coords.push(coord); @@ -554,7 +553,7 @@ export class MapBasesView extends BasesView { try { const parsed = JSON.parse(value.toString().trim()) as unknown[]; if (Array.isArray(parsed)) { - const coords: [number, number][] = []; + const coords: LatLng.Verified[] = []; for (const item of parsed) { const coord = this.parseLatLng(item); if (coord) coords.push(coord); From f1ed5123fabd876b08ab14a2f4f0489a8dd75fed Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 16:57:59 -0500 Subject: [PATCH 12/20] refactor: centralize parsing --- src/latlng.ts | 86 +++++++++++++++++++++++-------------- src/views/map-bases-view.ts | 66 ++++------------------------ 2 files changed, 61 insertions(+), 91 deletions(-) diff --git a/src/latlng.ts b/src/latlng.ts index ceb5c66..0b1177a 100644 --- a/src/latlng.ts +++ b/src/latlng.ts @@ -13,43 +13,44 @@ export namespace LatLng { readonly __brand: 'LatLng'; }; - // Validation function that narrows the type - export function parse(input: Like): Verified | null { - let lat: number; - let lng: number; - - if (typeof input === 'string') { - const parts = input.split(',').map(s => parseFloat(s.trim())); - if (parts.length !== 2) return null; - [lat, lng] = parts; - } else if (Array.isArray(input)) { - [lat, lng] = input; - } else if ('lat' in input && 'lng' in input) { - lat = input.lat; - lng = input.lng; - } else if ('latitude' in input && 'longitude' in input) { - lat = input.latitude; - lng = input.longitude; - } else { - return null; + // Main parse function - handles Obsidian-specific types (ListValue, StringValue, arrays, strings) + export function parse(value: unknown): Verified | null { + // Handle ListValue from frontmatter: [40.7128, -74.0060] + if (value && typeof value === 'object' && 'length' in value && typeof value.length === 'function') { + const listValue = value as { length: () => number; get: (index: number) => unknown }; + if (listValue.length() >= 2) { + const lat = parseCoordinate(listValue.get(0)); + const lng = parseCoordinate(listValue.get(1)); + if (lat !== null && lng !== null) { + return from(lat, lng); + } + } } - // Validation - if ( - typeof lat !== 'number' || - typeof lng !== 'number' || - !isFinite(lat) || - !isFinite(lng) || - lat < -90 || - lat > 90 || - lng < -180 || - lng > 180 - ) { - return null; + // Handle plain JavaScript array from JSON.parse: [40.7128, -74.0060] + if (Array.isArray(value) && value.length >= 2) { + const lat = parseCoordinate(value[0]); + const lng = parseCoordinate(value[1]); + if (lat !== null && lng !== null) { + return from(lat, lng); + } } - // Return branded type - return { lat, lng, __brand: 'LatLng' } as Verified; + // Handle string: "40.7128, -74.0060" or StringValue wrapper + if (value && typeof value === 'object' && 'toString' in value) { + const str = (value as { toString: () => string }).toString(); + const parts = str.split(',').map(p => parseFloat(p.trim())); + if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { + return from(parts[0], parts[1]); + } + } else if (typeof value === 'string') { + const parts = value.split(',').map(p => parseFloat(p.trim())); + if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { + return from(parts[0], parts[1]); + } + } + + return null; } // Parse or return default value @@ -138,4 +139,23 @@ export namespace LatLng { export function fromUnsafe(lat: number, lng: number): Verified { return { lat, lng, __brand: 'LatLng' } as Verified; } + + // Parse coordinate value from various sources (Obsidian-specific) + // Handles NumberValue, StringValue, and primitive types + export function parseCoordinate(value: unknown): number | null { + // Handle Obsidian's NumberValue/StringValue types + if (value && typeof value === 'object' && 'toString' in value) { + const strValue = (value as { toString: () => string }).toString(); + const num = parseFloat(strValue); + return isNaN(num) ? null : num; + } + if (typeof value === 'string') { + const num = parseFloat(value); + return isNaN(num) ? null : num; + } + if (typeof value === 'number') { + return isNaN(value) ? null : value; + } + return null; + } } diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index c10c44a..15c4b00 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -113,7 +113,7 @@ export class MapBasesView extends BasesView { private getCenter(): LatLng.Verified { const centerRaw = this.config.get('center'); if (!centerRaw) return LatLng.fromUnsafe(0, 0); - const parsed = this.parseLatLng(centerRaw); + const parsed = LatLng.parse(centerRaw); return parsed ?? LatLng.fromUnsafe(0, 0); } @@ -151,12 +151,12 @@ export class MapBasesView extends BasesView { if(this.hasConfigPropertyChanged('center')) { const centerRaw = this.config.get('center'); // Meaningful if it's empty OR if it's valid - if (!centerRaw || this.parseLatLng(centerRaw) !== null) { + if (!centerRaw || LatLng.parse(centerRaw) !== null) { return true; } } if (this.hasConfigPropertyChanged('defaultZoom')) { - const center = this.parseLatLng(this.config.get('center')); + const center = LatLng.parse(this.config.get('center')); // Only meaningful if center is valid AND non-empty (non-zero) if (center !== null && (center.lat !== 0 || center.lng !== 0)) { return true; @@ -439,7 +439,7 @@ export class MapBasesView extends BasesView { if (coordinatesProp) { try { const value = entry.getValue(coordinatesProp); - const coords = this.parseLatLng(value); + const coords = LatLng.parse(value); if (coords) return coords; } catch (error) { console.error(`Error extracting coordinates for ${entry.file.name}:`, error); @@ -455,8 +455,8 @@ export class MapBasesView extends BasesView { const lngValue = this.extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.lngKey); if (latValue !== undefined && lngValue !== undefined) { - const lat = this.parseCoordinate(latValue); - const lng = this.parseCoordinate(lngValue); + const lat = LatLng.parseCoordinate(latValue); + const lng = LatLng.parseCoordinate(lngValue); if (lat !== null && lng !== null) { return LatLng.from(lat, lng); } @@ -486,63 +486,13 @@ export class MapBasesView extends BasesView { return frontmatter[key]; } - private parseCoordinate(value: unknown): number | null { - if (value instanceof NumberValue) { - const numData = Number(value.toString()); - return isNaN(numData) ? null : numData; - } - if (value instanceof StringValue) { - const num = parseFloat(value.toString()); - return isNaN(num) ? null : num; - } - if (typeof value === 'string') { - const num = parseFloat(value); - return isNaN(num) ? null : num; - } - if (typeof value === 'number') { - return isNaN(value) ? null : value; - } - return null; - } - - private parseLatLng(value: unknown): LatLng.Verified | null { - // Handle ListValue from frontmatter: [40.7128, -74.0060] - if (value instanceof ListValue && value.length() >= 2) { - const lat = this.parseCoordinate(value.get(0)); - const lng = this.parseCoordinate(value.get(1)); - if (lat !== null && lng !== null) { - return LatLng.from(lat, lng); - } - } - - // Handle plain JavaScript array from JSON.parse: [40.7128, -74.0060] - if (Array.isArray(value) && value.length >= 2) { - const lat = this.parseCoordinate(value[0]); - const lng = this.parseCoordinate(value[1]); - if (lat !== null && lng !== null) { - return LatLng.from(lat, lng); - } - } - - // Handle string: "40.7128, -74.0060" or StringValue wrapper - if (value instanceof StringValue || typeof value === 'string') { - const str = value instanceof StringValue ? value.toString() : value; - const parts = str.split(',').map(p => parseFloat(p.trim())); - if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { - return LatLng.from(parts[0], parts[1]); - } - } - - return null; - } - private extractPolygonCoordinates(value: unknown): LatLng.Verified[] | null { try { // Handle ListValue (array from frontmatter) if (value instanceof ListValue) { const coords: LatLng.Verified[] = []; for (let i = 0; i < value.length(); i++) { - const coord = this.parseLatLng(value.get(i)); + const coord = LatLng.parse(value.get(i)); if (coord) coords.push(coord); } return coords.length > 0 ? coords : null; @@ -555,7 +505,7 @@ export class MapBasesView extends BasesView { if (Array.isArray(parsed)) { const coords: LatLng.Verified[] = []; for (const item of parsed) { - const coord = this.parseLatLng(item); + const coord = LatLng.parse(item); if (coord) coords.push(coord); } return coords.length > 0 ? coords : null; From 5998c99cbeab8313a5d21c973f5e94871be9b6bf Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 17:05:10 -0500 Subject: [PATCH 13/20] refactor: debloat main base view --- src/map-renderer.ts | 2 +- src/pointutils.ts | 2 +- src/{ => types}/latlng.ts | 7 +++- src/utils.ts | 16 ++++++++ src/views/map-bases-view.ts | 78 ++++++++++++++----------------------- 5 files changed, 54 insertions(+), 51 deletions(-) rename src/{ => types}/latlng.ts (97%) create mode 100644 src/utils.ts diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 7b474cd..0293ab3 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -9,7 +9,7 @@ import { MapView as MapViewType } from '@deck.gl/core'; import { MapTagSettings } from './settings/map-tag-settings'; import type MapPlugin from './main'; import type { ThumbnailCacheManager } from './thumbnail-cache'; -import { LatLng } from './latlng'; +import { LatLng } from './types/latlng'; function easeCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; diff --git a/src/pointutils.ts b/src/pointutils.ts index 303fe81..4f152b5 100644 --- a/src/pointutils.ts +++ b/src/pointutils.ts @@ -1,5 +1,5 @@ import { MapPoint } from "./map-renderer"; -import { LatLng } from "./latlng"; +import { LatLng } from "./types/latlng"; export function haveLocationsChanged(points1: MapPoint[], points2: MapPoint[]): boolean { return !arePointsEqual(points1, points2); diff --git a/src/latlng.ts b/src/types/latlng.ts similarity index 97% rename from src/latlng.ts rename to src/types/latlng.ts index 0b1177a..920fc27 100644 --- a/src/latlng.ts +++ b/src/types/latlng.ts @@ -143,8 +143,13 @@ export namespace LatLng { // Parse coordinate value from various sources (Obsidian-specific) // Handles NumberValue, StringValue, and primitive types export function parseCoordinate(value: unknown): number | null { + // Handle null/undefined + if (value === null || value === undefined) { + return null; + } + // Handle Obsidian's NumberValue/StringValue types - if (value && typeof value === 'object' && 'toString' in value) { + if (typeof value === 'object' && 'toString' in value) { const strValue = (value as { toString: () => string }).toString(); const num = parseFloat(strValue); return isNaN(num) ? null : num; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..bb1da40 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,16 @@ + + +export function extractFromFrontmatter(frontmatter: Record, key: string): unknown { + const arrayMatch = key.match(/^(.+)\[(\d+)\]$/); + if (arrayMatch) { + const arrayKey = arrayMatch[1]; + const index = parseInt(arrayMatch[2]); + const arrayValue = frontmatter[arrayKey]; + if (Array.isArray(arrayValue) && index >= 0 && index < arrayValue.length) { + return arrayValue[index]; + } + return undefined; + } + + return frontmatter[key]; +} \ No newline at end of file diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 15c4b00..64f5bca 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -15,9 +15,11 @@ import { createMapRenderer, MapPoint, updateMapPoints } from '../map-renderer'; import MapPlugin from '../main'; import { haveLocationsChanged } from '../pointutils'; import { currentViewState } from '../map-renderer'; -import { LatLng } from '../latlng'; +import { LatLng } from '../types/latlng'; +import { extractFromFrontmatter } from '../utils'; export const MapBasesViewType = 'map'; +export const SEARCH_DEBOUNCE_TIME = 300; const DEFAULT_MAP_HEIGHT = 400; const DEFAULT_MAP_ZOOM = 4; @@ -449,18 +451,13 @@ export class MapBasesView extends BasesView { // Fallback to global settings if coordinates property not set or failed if (this.plugin.settings.latKey && this.plugin.settings.lngKey) { try { + //TODO: use fm cache - location[0] and location[1] not accessible via entry.getValue? const fileCache = this.app.metadataCache.getFileCache(entry.file); if (fileCache?.frontmatter) { - const latValue = this.extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.latKey); - const lngValue = this.extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.lngKey); + const latValue = extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.latKey); + const lngValue = extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.lngKey); - if (latValue !== undefined && lngValue !== undefined) { - const lat = LatLng.parseCoordinate(latValue); - const lng = LatLng.parseCoordinate(lngValue); - if (lat !== null && lng !== null) { - return LatLng.from(lat, lng); - } - } + return LatLng.parse(`${latValue}, ${lngValue}`); } } catch (error) { console.error(`Error extracting coordinates from frontmatter for ${entry.file.name}:`, error); @@ -470,55 +467,40 @@ export class MapBasesView extends BasesView { return null; } - private extractFromFrontmatter(frontmatter: Record, key: string): unknown { - const arrayMatch = key.match(/^(.+)\[(\d+)\]$/); - if (arrayMatch) { - const arrayKey = arrayMatch[1]; - const index = parseInt(arrayMatch[2]); - const arrayValue = frontmatter[arrayKey]; - if (Array.isArray(arrayValue) && index >= 0 && index < arrayValue.length) { - return arrayValue[index]; - } - return undefined; - } - - // Regular property access - return frontmatter[key]; - } - private extractPolygonCoordinates(value: unknown): LatLng.Verified[] | null { try { + let items: unknown[] = []; + // Handle ListValue (array from frontmatter) if (value instanceof ListValue) { - const coords: LatLng.Verified[] = []; for (let i = 0; i < value.length(); i++) { - const coord = LatLng.parse(value.get(i)); - if (coord) coords.push(coord); + items.push(value.get(i)); } - return coords.length > 0 ? coords : null; + } + // Handle string value as JSON array + else if (value instanceof StringValue) { + const parsed = JSON.parse(value.toString().trim()); + if (Array.isArray(parsed)) { + items = parsed; + } + } + // Handle plain array + else if (Array.isArray(value)) { + items = value; } - // Handle string value as JSON array - if (value instanceof StringValue) { - try { - const parsed = JSON.parse(value.toString().trim()) as unknown[]; - if (Array.isArray(parsed)) { - const coords: LatLng.Verified[] = []; - for (const item of parsed) { - const coord = LatLng.parse(item); - if (coord) coords.push(coord); - } - return coords.length > 0 ? coords : null; - } - } catch { - // Not JSON, ignore - } + // Parse each item as a coordinate + const coords: LatLng.Verified[] = []; + for (const item of items) { + const coord = LatLng.parse(item); + if (coord) coords.push(coord); } + + return coords.length > 0 ? coords : null; } catch (error) { console.error('Error extracting polygon coordinates:', error); + return null; } - - return null; } private createSearchBox(): void { @@ -620,7 +602,7 @@ export class MapBasesView extends BasesView { searchTimeout = window.setTimeout(() => { void performSearch(query); - }, 300); + }, SEARCH_DEBOUNCE_TIME); }); // Close results when clicking outside From da11401e638531028861b29f5ff27d6811ffcd02 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 17:14:06 -0500 Subject: [PATCH 14/20] refactor: centralize `MapPoint` --- src/map-renderer.ts | 22 +------- src/pointutils.ts | 43 --------------- src/types/MapPoint.ts | 82 ++++++++++++++++++++++++++++ src/views/map-bases-view.ts | 8 +-- src/views/map-timeline-bases-view.ts | 2 +- 5 files changed, 89 insertions(+), 68 deletions(-) delete mode 100644 src/pointutils.ts create mode 100644 src/types/MapPoint.ts diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 0293ab3..25ff582 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -9,33 +9,15 @@ import { MapView as MapViewType } from '@deck.gl/core'; import { MapTagSettings } from './settings/map-tag-settings'; import type MapPlugin from './main'; import type { ThumbnailCacheManager } from './thumbnail-cache'; -import { LatLng } from './types/latlng'; +import { LatLng } from './types/LatLng'; +import { MapPoint } from './types/MapPoint'; function easeCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } -type PropertyValue = string | number | boolean | string[] | null; - export let currentViewState: MapViewState | null = null; -interface MapProperty { - name: string; - value: PropertyValue; -} - -export interface MapPoint { - location: LatLng.Verified; - title: string; - color?: string; - size?: number; - cover?: string; - file?: TFile; - tags?: string[]; - properties?: MapProperty[]; - polygon?: LatLng.Verified[]; -} - interface TileIndex { index: { x: number; diff --git a/src/pointutils.ts b/src/pointutils.ts deleted file mode 100644 index 4f152b5..0000000 --- a/src/pointutils.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MapPoint } from "./map-renderer"; -import { LatLng } from "./types/latlng"; - -export function haveLocationsChanged(points1: MapPoint[], points2: MapPoint[]): boolean { - return !arePointsEqual(points1, points2); -} - -export function arePointsEqual(points1: MapPoint[], points2: MapPoint[]): boolean { - if (points1.length !== points2.length) return false; - - for (let i = 0; i < points1.length; i++) { - const p1 = points1[i]; - const p2 = points2[i]; - - if (!LatLng.equals(p1.location, p2.location) || - p1.title !== p2.title || - p1.color !== p2.color || - p1.size !== p2.size || - p1.cover !== p2.cover || - p1.file?.path !== p2.file?.path) { - return false; - } - - const tags1 = p1.tags || []; - const tags2 = p2.tags || []; - if (tags1.length !== tags2.length || !tags1.every((tag, idx) => tag === tags2[idx])) { - return false; - } - - const props1 = p1.properties || []; - const props2 = p2.properties || []; - if (props1.length !== props2.length) { - return false; - } - for (let j = 0; j < props1.length; j++) { - if (props1[j].name !== props2[j].name || props1[j].value !== props2[j].value) { - return false; - } - } - } - - return true; -} \ No newline at end of file diff --git a/src/types/MapPoint.ts b/src/types/MapPoint.ts new file mode 100644 index 0000000..e3336ee --- /dev/null +++ b/src/types/MapPoint.ts @@ -0,0 +1,82 @@ +import { TFile } from "obsidian"; +import { LatLng } from "./LatLng"; + +type PropertyValue = string | number | boolean | string[] | null; + +interface MapProperty { + name: string; + value: PropertyValue; +} + +export interface MapPoint { + location: LatLng.Verified; + title: string; + color?: string; + size?: number; + cover?: string; + file?: TFile; + tags?: string[]; + properties?: MapProperty[]; + polygon?: LatLng.Verified[]; +} + +export namespace MapPoint { + export function areSamePlace(point1: MapPoint, point2: MapPoint): boolean { + return point1.location.lat === point2.location.lat && point1.location.lng === point2.location.lng; + } + + export function areEqual(point1: MapPoint[], point2: MapPoint[]): boolean; + export function areEqual(point1: MapPoint, point2: MapPoint): boolean; + + export function areEqual( + point1: MapPoint | MapPoint[], + point2: MapPoint | MapPoint[] + ): boolean { + // Handle array case + if (Array.isArray(point1) && Array.isArray(point2)) { + if (point1.length !== point2.length) return false; + for (let i = 0; i < point1.length; i++) { + if (!areEqual(point1[i], point2[i])) return false; + } + return true; + } + + // Handle single MapPoint case + if (!Array.isArray(point1) && !Array.isArray(point2)) { + if (!LatLng.equals(point1.location, point2.location) || + point1.title !== point2.title || + point1.color !== point2.color || + point1.size !== point2.size || + point1.cover !== point2.cover || + point1.file?.path !== point2.file?.path) { + return false; + } + + const tags1 = point1.tags || []; + const tags2 = point2.tags || []; + if (tags1.length !== tags2.length || !tags1.every((tag, idx) => tag === tags2[idx])) { + return false; + } + + const props1 = point1.properties || []; + const props2 = point2.properties || []; + if (props1.length !== props2.length) { + return false; + } + for (let j = 0; j < props1.length; j++) { + if (props1[j].name !== props2[j].name || props1[j].value !== props2[j].value) { + return false; + } + } + + return true; + } + + return false; + } + + export function haveLocationsChanged(points1: MapPoint[], points2: MapPoint[]): boolean { + return !areEqual(points1, points2); + } +} + diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 64f5bca..04e8b5b 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -11,12 +11,12 @@ import { } from 'obsidian'; import { Deck, FlyToInterpolator, MapViewState } from '@deck.gl/core'; import { MapView as MapViewType } from '@deck.gl/core'; -import { createMapRenderer, MapPoint, updateMapPoints } from '../map-renderer'; +import { createMapRenderer, updateMapPoints } from '../map-renderer'; import MapPlugin from '../main'; -import { haveLocationsChanged } from '../pointutils'; import { currentViewState } from '../map-renderer'; -import { LatLng } from '../types/latlng'; +import { LatLng } from '../types/LatLng'; import { extractFromFrontmatter } from '../utils'; +import { MapPoint } from '../types/MapPoint'; export const MapBasesViewType = 'map'; export const SEARCH_DEBOUNCE_TIME = 300; @@ -339,7 +339,7 @@ export class MapBasesView extends BasesView { const center = this.getCenter(); const hasConfiguredCenter = center.lat !== 0 || center.lng !== 0; - const locationsChanged = haveLocationsChanged(points, this.lastPoints); + const locationsChanged = MapPoint.haveLocationsChanged(points, this.lastPoints); const configChanged = this.hasConfigMeaningfullyChanged(); this.lastConfigState = { ...this.config.data }; diff --git a/src/views/map-timeline-bases-view.ts b/src/views/map-timeline-bases-view.ts index 19bb0fa..4a936c0 100644 --- a/src/views/map-timeline-bases-view.ts +++ b/src/views/map-timeline-bases-view.ts @@ -5,7 +5,7 @@ import { ViewOption, setIcon, } from 'obsidian'; -import { MapPoint } from '../map-renderer'; +import { MapPoint } from '../types/MapPoint'; import MapPlugin from '../main'; import { MapBasesView } from './map-bases-view'; From 1cefcb28294bed9a459140b5f374aa9051da4e50 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 17:18:40 -0500 Subject: [PATCH 15/20] feat: toggleable search box --- src/settings/map-settings.ts | 13 ++++++++++++- src/views/map-bases-view.ts | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/settings/map-settings.ts b/src/settings/map-settings.ts index 334d194..079cc32 100644 --- a/src/settings/map-settings.ts +++ b/src/settings/map-settings.ts @@ -13,6 +13,7 @@ export interface MapPluginSettings { thumbnailTargetSize: number; tagSettings: MapTagSettings; enableTimelineView: boolean; + enableSearchGeocoding: boolean; } export const DEFAULT_SETTINGS: MapPluginSettings = { @@ -25,7 +26,8 @@ export const DEFAULT_SETTINGS: MapPluginSettings = { enableThumbnailCache: false, thumbnailTargetSize: 25, tagSettings: DEFAULT_MAP_TAG_SETTINGS, - enableTimelineView: false + enableTimelineView: false, + enableSearchGeocoding: false }; export class MapSettingTab extends PluginSettingTab { @@ -120,6 +122,15 @@ export class MapSettingTab extends PluginSettingTab { this.plugin.settings.transitionDuration = value; await this.plugin.saveSettings(); })); + new Setting(containerEl) + .setName('Enable search geocoding') + .setDesc('Enable a search bar to go to locations via natural language or coordinate paste') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableSearchGeocoding) + .onChange(async (value) => { + this.plugin.settings.enableSearchGeocoding = value; + await this.plugin.saveSettings(); + })); new Setting(containerEl) .setName('Performance') diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 04e8b5b..eef1b6d 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -306,7 +306,9 @@ export class MapBasesView extends BasesView { this.containerEl.removeClass('is-loading'); - this.createSearchBox(); + if (this.plugin.settings.enableSearchGeocoding) { + this.createSearchBox(); + } setTimeout(() => { if (!tilesLoaded) { From 2e410a0e383701c469cb8d8d8f70afa8a57deeef Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 17:23:41 -0500 Subject: [PATCH 16/20] feat: coordinate paste support --- src/settings/map-settings.ts | 1 + src/views/map-bases-view.ts | 46 +++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/settings/map-settings.ts b/src/settings/map-settings.ts index 079cc32..a68cd08 100644 --- a/src/settings/map-settings.ts +++ b/src/settings/map-settings.ts @@ -129,6 +129,7 @@ export class MapSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.enableSearchGeocoding) .onChange(async (value) => { this.plugin.settings.enableSearchGeocoding = value; + this.plugin.refreshAllMapViews(); await this.plugin.saveSettings(); })); diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index eef1b6d..787d9a0 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -589,9 +589,53 @@ export class MapBasesView extends BasesView { } }; + const handleCoordinatePaste = (query: string): boolean => { + const parsed = LatLng.parse(query); + if (parsed && this.deck) { + this.deck.setProps({ + initialViewState: { + MapView: { + latitude: parsed.lat, + longitude: parsed.lng, + zoom: 12, + transitionDuration: 800, + transitionInterpolator: new FlyToInterpolator(), + } + } + }); + searchInput.value = LatLng.toString(parsed); + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + return true; + } + return false; + }; + + searchInput.addEventListener('paste', (e) => { + e.preventDefault(); + const pastedText = e.clipboardData?.getData('text')?.trim() || ''; + + if (handleCoordinatePaste(pastedText)) { + return; + } + + searchInput.value = pastedText; + searchInput.dispatchEvent(new Event('input')); + }); + + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const query = searchInput.value.trim(); + + if (handleCoordinatePaste(query)) { + return; + } + } + }); + searchInput.addEventListener('input', () => { const query = searchInput.value.trim(); - + if (searchTimeout) { window.clearTimeout(searchTimeout); } From c85125cdb4acb296d38ce14e60ac280415596906 Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 17:41:35 -0500 Subject: [PATCH 17/20] feat: select results from search dropdown --- src/views/map-bases-view.ts | 83 +++++++++++++++++++++++++++++++++---- styles.css | 3 +- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 787d9a0..3d29abd 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -20,6 +20,7 @@ import { MapPoint } from '../types/MapPoint'; export const MapBasesViewType = 'map'; export const SEARCH_DEBOUNCE_TIME = 300; +export const SEARCH_TRANSITION_DURATION = 1600; const DEFAULT_MAP_HEIGHT = 400; const DEFAULT_MAP_ZOOM = 4; @@ -535,6 +536,7 @@ export class MapBasesView extends BasesView { let searchTimeout: number; let currentResults: Array<{ lat: number; lng: number; name: string; display_name: string }> = []; + let selectedResultIndex = -1; const performSearch = async (query: string) => { try { @@ -553,13 +555,14 @@ export class MapBasesView extends BasesView { })); resultsContainer.empty(); - + selectedResultIndex = -1; + if (currentResults.length > 0) { - currentResults.forEach((result) => { + currentResults.forEach((result, index) => { const resultItem = resultsContainer.createDiv({ cls: 'map-search-result-item' }); resultItem.textContent = result.display_name; - - resultItem.addEventListener('click', () => { + + const selectResult = () => { if (this.deck) { this.deck.setProps({ initialViewState: { @@ -567,7 +570,7 @@ export class MapBasesView extends BasesView { latitude: result.lat, longitude: result.lng, zoom: 12, - transitionDuration: 800, + transitionDuration: SEARCH_TRANSITION_DURATION, transitionInterpolator: new FlyToInterpolator(), } } @@ -576,6 +579,12 @@ export class MapBasesView extends BasesView { searchInput.value = result.name || result.display_name; resultsContainer.empty(); resultsContainer.removeClass('visible'); + selectedResultIndex = -1; + }; + + resultItem.addEventListener('click', selectResult); + resultItem.addEventListener('mouseenter', () => { + updateSelectedResult(index); }); }); resultsContainer.addClass('visible'); @@ -589,6 +598,18 @@ export class MapBasesView extends BasesView { } }; + const updateSelectedResult = (index: number) => { + selectedResultIndex = index; + const resultItems = resultsContainer.querySelectorAll('.map-search-result-item'); + resultItems.forEach((item, i) => { + if (i === index) { + item.addClass('selected'); + } else { + item.removeClass('selected'); + } + }); + }; + const handleCoordinatePaste = (query: string): boolean => { const parsed = LatLng.parse(query); if (parsed && this.deck) { @@ -598,7 +619,7 @@ export class MapBasesView extends BasesView { latitude: parsed.lat, longitude: parsed.lng, zoom: 12, - transitionDuration: 800, + transitionDuration: SEARCH_TRANSITION_DURATION, transitionInterpolator: new FlyToInterpolator(), } } @@ -606,6 +627,7 @@ export class MapBasesView extends BasesView { searchInput.value = LatLng.toString(parsed); resultsContainer.empty(); resultsContainer.removeClass('visible'); + selectedResultIndex = -1; return true; } return false; @@ -624,12 +646,50 @@ export class MapBasesView extends BasesView { }); searchInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (currentResults.length > 0) { + const newIndex = selectedResultIndex < currentResults.length - 1 ? selectedResultIndex + 1 : selectedResultIndex; + updateSelectedResult(newIndex); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (currentResults.length > 0 && selectedResultIndex > 0) { + updateSelectedResult(selectedResultIndex - 1); + } + } else if (e.key === 'Enter') { + e.preventDefault(); const query = searchInput.value.trim(); if (handleCoordinatePaste(query)) { return; } + + // If a result is selected, navigate to it + if (selectedResultIndex >= 0 && selectedResultIndex < currentResults.length) { + const result = currentResults[selectedResultIndex]; + if (this.deck) { + this.deck.setProps({ + initialViewState: { + MapView: { + latitude: result.lat, + longitude: result.lng, + zoom: 12, + transitionDuration: SEARCH_TRANSITION_DURATION, + transitionInterpolator: new FlyToInterpolator(), + } + } + }); + } + searchInput.value = result.name || result.display_name; + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + selectedResultIndex = -1; + } + } else if (e.key === 'Escape') { + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + selectedResultIndex = -1; } }); @@ -646,6 +706,13 @@ export class MapBasesView extends BasesView { return; } + // Don't perform geocoding search if it's a valid LatLng + if (LatLng.parse(query)) { + resultsContainer.empty(); + resultsContainer.removeClass('visible'); + return; + } + searchTimeout = window.setTimeout(() => { void performSearch(query); }, SEARCH_DEBOUNCE_TIME); @@ -654,7 +721,9 @@ export class MapBasesView extends BasesView { // Close results when clicking outside document.addEventListener('click', (e) => { if (!searchContainer.contains(e.target as Node)) { + resultsContainer.empty(); resultsContainer.removeClass('visible'); + selectedResultIndex = -1; } }); } diff --git a/styles.css b/styles.css index 12d1352..ab25bd9 100644 --- a/styles.css +++ b/styles.css @@ -627,6 +627,7 @@ border-bottom: none; } -.map-search-result-item:hover { +.map-search-result-item:hover, +.map-search-result-item.selected { background: var(--background-modifier-hover); } \ No newline at end of file From d491fe0171efa13ce69f4358057e494d4ad36b5a Mon Sep 17 00:00:00 2001 From: ccmdi Date: Sun, 23 Nov 2025 18:40:23 -0500 Subject: [PATCH 18/20] chore: eslint --- src/types/MapPoint.ts | 1 + src/types/latlng.ts | 1 + src/views/map-bases-view.ts | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/types/MapPoint.ts b/src/types/MapPoint.ts index e3336ee..f93cbf2 100644 --- a/src/types/MapPoint.ts +++ b/src/types/MapPoint.ts @@ -20,6 +20,7 @@ export interface MapPoint { polygon?: LatLng.Verified[]; } +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace MapPoint { export function areSamePlace(point1: MapPoint, point2: MapPoint): boolean { return point1.location.lat === point2.location.lat && point1.location.lng === point2.location.lng; diff --git a/src/types/latlng.ts b/src/types/latlng.ts index 920fc27..3345699 100644 --- a/src/types/latlng.ts +++ b/src/types/latlng.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace LatLng { // Unverified input - accepts various formats export type Like = diff --git a/src/views/map-bases-view.ts b/src/views/map-bases-view.ts index 3d29abd..31bae4f 100644 --- a/src/views/map-bases-view.ts +++ b/src/views/map-bases-view.ts @@ -3,7 +3,6 @@ import { BasesPropertyId, BasesView, ListValue, - NumberValue, QueryController, StringValue, ViewOption, @@ -460,7 +459,7 @@ export class MapBasesView extends BasesView { const latValue = extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.latKey); const lngValue = extractFromFrontmatter(fileCache.frontmatter, this.plugin.settings.lngKey); - return LatLng.parse(`${latValue}, ${lngValue}`); + return LatLng.parse([latValue, lngValue]); } } catch (error) { console.error(`Error extracting coordinates from frontmatter for ${entry.file.name}:`, error); @@ -482,7 +481,7 @@ export class MapBasesView extends BasesView { } // Handle string value as JSON array else if (value instanceof StringValue) { - const parsed = JSON.parse(value.toString().trim()); + const parsed = JSON.parse(value.toString().trim()) as unknown[]; if (Array.isArray(parsed)) { items = parsed; } From d183ecab6569a565ff49dff2374990b2161f987b Mon Sep 17 00:00:00 2001 From: ccmdi Date: Mon, 24 Nov 2025 20:03:59 -0500 Subject: [PATCH 19/20] chore: README --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e1d123..4e6cd9f 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,40 @@ Higher tags take precedence in locations with multiple tags. The timeline view type filters map markers by date. - - **Date property**: Property containing dates (supports `YYYY-MM-DD`, `YYYY-MM`, `YYYY`, and negative years like `-1000-01-01` for BC) - **Group by property**: Deduplicate markers with the same value - for tracking the same object at different times - **Grouping uniqueness mode**: Show all entries, only the most recent, or only the least recent per group (per date property) The slider and text box filter markers up to a specific date. You can manually type in the text box the exact date you want to jump to. The granularity level is determined by the dropdown. + +### Using search and geocoding + +Enable "Search geocoding" in plugin settings to add a search bar to your map views. + +The search bar supports: +- **Natural language search**: Type location names and select from search results +- **Coordinate paste**: Paste coordinates in various formats: + - Decimal degrees: `40.7128, -74.0060` + - Degrees/minutes/seconds formats + - Google Maps links + - Other common coordinate formats + +Select a result from the dropdown to navigate to that location on the map. + +### Customizing map appearance + +In plugin settings under "Map": + +- **Stroke width for icons**: Adjust the outline thickness of map markers (0.5-5) +- **Fill icons**: Toggle whether icons are filled or outlined +- **Auto-center on update**: Automatically zoom and center the map when data changes +- **Transition duration**: Control the animation speed when the map view changes (0-2000ms) + +### Performance optimization + +**Thumbnail cache**: Enable in settings under "Performance" to cache downsized versions of location cover images for instant tooltips. + +- **Thumbnail target size**: Set the target file size for cached thumbnails (10-50 KB) +- **Cache status**: View number of cached thumbnails and total cache size +- **Rebuild cache**: Regenerate all thumbnails if images have changed +- **Clear cache**: Delete all cached thumbnails From e5e1c3263b1932b6c3e960d5ef0b4c050527e99c Mon Sep 17 00:00:00 2001 From: ccmdi Date: Mon, 24 Nov 2025 20:12:58 -0500 Subject: [PATCH 20/20] refactor: use `boundingBox` --- src/map-renderer.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/map-renderer.ts b/src/map-renderer.ts index 25ff582..7e77658 100644 --- a/src/map-renderer.ts +++ b/src/map-renderer.ts @@ -449,11 +449,9 @@ export function createMapRenderer(config: MapRendererOptions): Deck; }) => { - //TODO: replace with boundingBox - // eslint-disable-next-line @typescript-eslint/no-deprecated - const bbox = props.tile.bbox; - if (!('west' in bbox)) return null; - const { west, south, east, north } = bbox; + const boundingBox = props.tile.boundingBox; + if (!boundingBox || boundingBox.length !== 2) return null; + const [[west, south], [east, north]] = boundingBox; return new BitmapLayer({ ...props, data: undefined,