This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Obsidian Map View is a sophisticated Obsidian plugin that transforms notes into an interactive geographic information system (GIS). It parses geolocation data from note frontmatter, inline links, and external files (GPX, KML, GeoJSON), renders them on interactive maps, and provides powerful querying, filtering, and display customization capabilities.
# Development with watch mode
npm run dev-dist
# Production build
npm run build
# Code formatting
npm run prettier
# Check code formatting
npm run stylecheck- TypeScript with Svelte 5 for UI components
- Rollup for bundling with plugins for TypeScript, Svelte, PostCSS, and images
- Leaflet ecosystem for mapping (leaflet, markercluster, geosearch, geoman, offline)
- boon-js for query language parsing
- @tmcw/togeojson for GPX/KML conversion
- FontAwesome for marker icons
- Obsidian API for plugin integration
-
Parsing (
src/geoHelpers.ts,src/geojsonParser.ts):matchInlineLocation(): Extracts[name](geo:lat,lng) tag:foopatterns from file contentgetFrontMatterLocation(): Readslocation:property from Obsidian metadata cachegetGeoJsonLayersFromFile(): Parses GeoJSON/GPX/KML files and inline geojson code blocks
-
Layer System (abstract base:
src/baseGeoLayer.ts):FileMarker(src/fileMarker.ts): Individual location markers from notesGeoJsonLayer(src/geojsonLayer.ts): Paths and shapes from GeoJSON data- Each logical layer maintains multiple Leaflet layer instances (one per map container) via
geoLayers: Map<containerId, leaflet.Layer>
-
Layer Cache (
src/layerCache.ts):- Plugin-global repository indexed by layer ID and file path
- Rebuilds affected layers when files change via
updateMarkersWithRelationToFile() - Initialization can be deferred until first map opens (
loadLayersAheadsetting)
-
Display Rules (
src/displayRulesCache.ts):- Query-based styling engine that applies icon properties, path options, and badges
- Rules applied in sequence; matching rules override previous properties
- Uses Query system (
src/query.ts) to match layers viatag:,path:,linkedfrom:, etc.
-
Map Rendering (
src/mapContainer.ts):filterAndPrepareMarkers(): Applies user query filters and builds link edgesupdateMapLayers(): Diffs old/new layers and updates Leaflet map (reuses unchanged layers)- Manages marker clusters, tile layers, and all UI controls
Main Plugin (src/main.ts):
- Entry point that registers views, commands, protocol handlers, and event listeners
- Maintains
allMapContainersregistry for all active map instances - Handles vault file events (create, modify, delete, rename) to trigger layer updates
- Provides global handlers for geolink interactions in editor
Map Views:
MainMapView(src/mainMapView.ts): Standalone full-featured map viewEmbeddedMap(src/embeddedMap.ts): Inline maps frommapviewcode blocks with state persistenceBasesMapView(src/basesMapView.ts): Integration with Obsidian BasesMapPreviewPopup(src/mapPreviewPopup.ts): Transient previews on geolink hover
Query System (src/query.ts):
- Boolean query language:
tag:#foo AND path:"bar" OR linkedfrom:"Trip Plan" - Parsed into RPN (reverse Polish notation) for fast evaluation
- Used for both display rule matching and user filtering
Display Rules (src/displayRulesCache.ts, src/markerIcons.ts):
- Rules composed of: query + icon details + path options + badges
IconFactory.getIconFromRules()creates Leaflet markers with FontAwesome icons- Badges add corner indicators (up to 4 per marker)
State Management (src/mapState.ts):
- Immutable
MapStateobject: position, zoom, query, display options mergeStates()for partial updates,areStatesEqual()for diffing- Persisted in embedded maps and presets
Editor Integration:
src/codemirrorViewPlugin.ts: Decorates inline geolinks with custom event handlerssrc/geoLinkReplacers.ts: Post-processes reading view to make geolinks clickablesrc/locationSuggest.ts: Autocomplete for location search in[](geo:)templatessrc/tagSuggest.ts: Tag autocomplete for queries
The plugin supports multiple simultaneous map views (main views, embeds, previews). Each logical layer (FileMarker or GeoJsonLayer) can exist as different Leaflet objects in different containers:
class BaseGeoLayer {
geoLayers: Map<string, leaflet.Layer> = new Map();
// Same geographic data, different visual representations per container
}This enables:
- Independent filtering per view (same note shown differently in two maps)
- Efficient reuse of layer data without duplication
- Container-specific display state (hover, selection)
- Editor updates are frequent:
updateMarkersWithRelationToFile()must be extremely efficient as it runs on every file change - Cluster groups: Nearby markers grouped to reduce DOM nodes (configurable "max cluster size")
- Lazy layer initialization: Cache built only when needed if
loadLayersAheadis false - Efficient diffing:
updateMapLayers()uses "touched" flag to identify add/remove operations, reuses unchanged layers withisSame() - Viewport-limited processing: Geolink decorations only applied to visible editor content
- Query pre-compilation: Queries compiled to RPN once, evaluated many times
- Main entry:
src/main.ts(MapViewPlugin class) - Layer system:
src/baseGeoLayer.ts,src/fileMarker.ts,src/geojsonLayer.ts - Parsing:
src/geoHelpers.ts,src/geojsonParser.ts - Query engine:
src/query.ts,src/displayRulesCache.ts - Map rendering:
src/mapContainer.ts,src/mapState.ts - Views:
src/mainMapView.ts,src/embeddedMap.ts,src/basesMapView.ts - Icons:
src/markerIcons.ts - Settings:
src/settings.ts,src/settingsTab.ts - Svelte UI:
src/components/*.svelte(controls, dialogs) - Styles:
src/css/*.css,src/less/*.less
- Add property to
iconDetails,pathOptions, orbadgeDetailsinsrc/markerIcons.ts - Update
EditDisplayRuleDialog.svelteto expose the property in UI - Modify
IconFactory.getIconFromRules()orDisplayRulesCache.runOn()to apply the property - Update
src/displayRulesCache.tsif composition logic changes
- Add operator constant to
query.ts:OPERATOR_NAME - Implement matching logic in
Query.testPredicate()switch statement - Add autocomplete support in
TagSuggest.getSuggestions()if needed - Document in README.md under "Queries" section
- Add regex pattern to
src/consts.tsor add URL parsing rule - Implement parser function in
src/geoHelpers.ts - Call parser in
getMarkersFromFileContent()or equivalent - Add tests if available (currently limited test coverage)
- Extend
AbstractMapViewfromsrc/abstractMapView.ts - Implement
getViewType(),getDisplayText(),getIcon() - Create and manage
MapContainerinstance inonOpen() - Register view in
main.tsviathis.registerView() - Add command to open view if needed
Rollup Configuration (rollup.config.js):
- Plugins: TypeScript, Svelte, CommonJS, Node Resolve, PostCSS, Image, Copy
- Environment:
BUILD=developmentdisables minification for faster dev builds - Output:
main.js(plugin code) +styles.css(compiled styles) - Source maps generated for debugging
TypeScript Config:
- Extends
@tsconfig/sveltefor Svelte 5 compatibility - Target: ES2022
- Strict mode disabled (legacy codebase)
- Module resolution: Node
Initialization (onload()):
- Load settings from
data.json - Initialize LayerCache (if
loadLayersAheadenabled) - Register views, commands, protocol handlers
- Setup vault event listeners (file create/modify/delete/rename)
- Initialize global tile cache for offline maps
- Register CodeMirror view plugin and markdown post-processors
- Add settings tab
File Change Handling:
- Obsidian fires metadata cache
changedevent - Plugin calls
updateMarkersWithRelationToFile(file) - LayerCache rebuilds layers for affected file
- All active MapContainers receive layer diff
- Each container calls
updateMapLayers()to reflect changes
Shutdown (onunload()):
- Destroy all active map containers
- Clear global registries and event handlers
- Enable "Developer Tools" in Obsidian settings
- Use
console.log()- output visible in DevTools console - Source maps available in development builds
- Breakpoints work in TypeScript source files
- Test changes by reloading plugin: Ctrl+P → "Reload app without saving"
This project has limited automated test coverage. When making changes:
- Manually test with sample vault containing various geolocation formats
- Verify embedded maps update correctly when notes change
- Check performance with large vaults (hundreds of geolocations)
- Test on both desktop and mobile if possible
- Validate display rules apply correctly with complex queries
- Layer cache must be rebuilt when files change: Always update via
updateMarkersWithRelationToFile() - Multiple map containers share layer cache: Changes affect all open views
- Display rules apply in order: Later matching rules override earlier ones
- Inline tags vs note tags: Inline tags (
tag:foo) only apply to specific markers, not whole note - Leaflet coordinates are
[lat, lng]: Beware of order when parsing user input - Settings changes require plugin reload: No hot-reload for plugin settings
- Svelte 5 runes syntax: Use
$state,$derived,$effectfor reactivity - All new UI should use Svelte 5, and not the vanilla JS style on which I started the plugin with, and slowly replacing.
plugin.settingsis a$statereactive proxy:loadSettings()wraps settings viamakeSettingsReactive()(src/settingsReactive.svelte.ts). This means deep mutations likesettings.mapControlsSections.foo = trueare automatically tracked by Svelte components that receivesettingsas a prop — no need forsettings = { ...settings }hacks to trigger re-renders.- Never use
settings = { ...settings }to trigger reactivity: This creates a shallow copy and breaks the reference toplugin.settings. Any subsequent mutation to the copy (e.g.settings.defaultState = x) will not reachplugin.settingsand will not be saved.