From d42a256ba513e676e1d30b501f7d661dfdb9e9cd Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 11:01:24 +0000 Subject: [PATCH 01/32] Made it easier to run the development build Obsidian could not find the built main.js file in the dist folder --- package.json | 2 +- rollup.config.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7d42504..0c58093 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "An interactive map view for Obsidian.md", "main": "main.js", "scripts": { - "dev": "rollup --config rollup.config.js -w", + "dev": "rollup --config rollup.config.js -w --environment BUILD:development", "build": "rollup --config rollup.config.js --environment BUILD:production" }, "keywords": [], diff --git a/rollup.config.js b/rollup.config.js index 3f58be0..d290ffb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -17,7 +17,7 @@ if you want to view the source visit the plugins github repository export default { input: 'src/main.ts', output: { - dir: './dist', + dir: process.env.BUILD === 'development' ? '.' : './dist', sourcemap: isProd ? false : 'inline', sourcemapExcludeSources: isProd, format: 'cjs', @@ -30,11 +30,13 @@ export default { nodeResolve({browser: true}), commonjs(), postcss({ extensions: ['.css'], plugins: [postcss_url({url: 'inline'})] }), - copy({ - targets: [ - { src: './manifest.json', dest: 'dist' }, - { src: './styles.css', dest: 'dist' } - ] - }) + ...(process.env.BUILD !== 'development' ? [ + copy({ + targets: [ + { src: './manifest.json', dest: 'dist' }, + { src: './styles.css', dest: 'dist' } + ] + }) + ] : []), ] }; From 48e960e83670ba0235d24be13120b02246ce3068 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 11:05:23 +0000 Subject: [PATCH 02/32] Moved images into an img folder to declutter the root --- README.md | 22 +++++++++--------- .../convert-command.png | Bin .../convert-to-location.png | Bin copy.png => img/copy.png | Bin custom-open-in.png => img/custom-open-in.png | Bin .../geosearch-suggest.gif | Bin intro.gif => img/intro.gif | Bin marker-rules.png => img/marker-rules.png | Bin new-note-popup.gif => img/new-note-popup.gif | Bin new-note.png => img/new-note.png | Bin open-in.png => img/open-in.png | Bin sample.png => img/sample.png | Bin search.png => img/search.png | Bin url-parsing.png => img/url-parsing.png | Bin 14 files changed, 11 insertions(+), 11 deletions(-) rename convert-command.png => img/convert-command.png (100%) rename convert-to-location.png => img/convert-to-location.png (100%) rename copy.png => img/copy.png (100%) rename custom-open-in.png => img/custom-open-in.png (100%) rename geosearch-suggest.gif => img/geosearch-suggest.gif (100%) rename intro.gif => img/intro.gif (100%) rename marker-rules.png => img/marker-rules.png (100%) rename new-note-popup.gif => img/new-note-popup.gif (100%) rename new-note.png => img/new-note.png (100%) rename open-in.png => img/open-in.png (100%) rename sample.png => img/sample.png (100%) rename search.png => img/search.png (100%) rename url-parsing.png => img/url-parsing.png (100%) diff --git a/README.md b/README.md index 58294c6..fd8570e 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ You can set different icons for different note types, filter the displayed notes It also provides a wide range of tools to add geolocations to your notes, including address searches and parsing of URLs from external sources such as Google Maps. -![](sample.png) +![](img/sample.png) -![](intro.gif) +![](img/intro.gif) The plugin's guiding philosophy and goal is to provide a **personal GIS system** as a complementary view for your notes. I wrote it because I wanted my ever-growing Zettelkasten to be able to answer questions like... @@ -93,7 +93,7 @@ Map View adds an Obsidian command named "New geolocation note", which you can ma This opens a dialog on which you can search (address or location based on your [configured geocoding provider](#changing-a-geocoding-provider)) or paste a URL using the built-in or custom [URL parsing rules](#url-parsing-rules). -![](new-note-popup.gif) +![](img/new-note-popup.gif) ### In an Existing Note @@ -113,15 +113,15 @@ The map offers several tools to create notes. 1. Use "new note here" when right-clicking the map. This will create a new note (based on the template you can change in the settings) with the location you clicked. You can create either an empty note with a front matter (single geolocation) or an empty note with an inline geolocation. -![](new-note.png) +![](img/new-note.png) Note that the map can be searched using the tool on the upper-right side. -![](search.png) +![](img/search.png) 2. If you prefer to enter geolocations as text, use one of the "copy geolocation" options when you right-click the map. If you use "copy geolocation", just remember you need the note to start with a front matter that has an empty `locations:` line. -![](copy.png) +![](img/copy.png) ## Paste as Geolocation @@ -155,7 +155,7 @@ A single marker is defined with a *tag pattern* and *icon details*. The tag pattern is usually a tag name (e.g. `#dogs`), but it can also be with a wildcard (e.g. `#trips/*`). Icon details are a few properties: icon name (taken from the Font Awesome catalog), color and shape. -![](marker-rules.png) +![](img/marker-rules.png) A single marker is defined in the following JSON structure: `{"prefix": "fas", "icon": "fa-bus", "shape": "circle", "color": "red"}` @@ -195,7 +195,7 @@ This command inserts an empty inline location template: `[](geo:)`. When editing an inline location in this format, whether if you added it manually or using the command, if you start entering a link name, Map View will start offering locations based on a geocoding service. Selecting one of the suggestions will fill-in the coordinates of the chosen locations, *not* change your link name (assuming you prefer your own name rather than the formal one offered by the geocoding service), and jump the cursor to beyond the link so you can continue typing. -![](geosearch-suggest.gif) +![](img/geosearch-suggest.gif) If your note is not yet marked as one including locations (by a `locations:`) tag in the front matter, this is added automatically. @@ -230,7 +230,7 @@ If a dark theme is detected, or if you specifically change the map source type t Many context menus of Map View display a customizable Open In list, which can open a given location in external sources. These sources can be Google Maps, OpenStreetMap, specialized mapping tools or pretty much anything you use for viewing locations. -![](open-in.png) +![](img/open-in.png) The Open In list is shown: - When right-clicking on the map. @@ -240,7 +240,7 @@ The Open In list is shown: This list can be edited through the plugin's settings menu, with a name that will be displayed in the context menus and a URL pattern. The URL pattern has two parameters -- `{x}` and `{y}` -- that will be replaced by the latitude and longitude of the clicked location. -![](custom-open-in.png) +![](img/custom-open-in.png) Popular choices may be: - Google Maps: `https://maps.google.com/?q={x},{y}` @@ -263,7 +263,7 @@ The syntax expects two captures group and you can configure if they are parsed a And if you think your added regular expressions are solid enough, please add them to the plugin using a PR so others can benefit! -![](url-parsing.png) +![](img/url-parsing.png) ## Relation to Other Obsidian Plugins diff --git a/convert-command.png b/img/convert-command.png similarity index 100% rename from convert-command.png rename to img/convert-command.png diff --git a/convert-to-location.png b/img/convert-to-location.png similarity index 100% rename from convert-to-location.png rename to img/convert-to-location.png diff --git a/copy.png b/img/copy.png similarity index 100% rename from copy.png rename to img/copy.png diff --git a/custom-open-in.png b/img/custom-open-in.png similarity index 100% rename from custom-open-in.png rename to img/custom-open-in.png diff --git a/geosearch-suggest.gif b/img/geosearch-suggest.gif similarity index 100% rename from geosearch-suggest.gif rename to img/geosearch-suggest.gif diff --git a/intro.gif b/img/intro.gif similarity index 100% rename from intro.gif rename to img/intro.gif diff --git a/marker-rules.png b/img/marker-rules.png similarity index 100% rename from marker-rules.png rename to img/marker-rules.png diff --git a/new-note-popup.gif b/img/new-note-popup.gif similarity index 100% rename from new-note-popup.gif rename to img/new-note-popup.gif diff --git a/new-note.png b/img/new-note.png similarity index 100% rename from new-note.png rename to img/new-note.png diff --git a/open-in.png b/img/open-in.png similarity index 100% rename from open-in.png rename to img/open-in.png diff --git a/sample.png b/img/sample.png similarity index 100% rename from sample.png rename to img/sample.png diff --git a/search.png b/img/search.png similarity index 100% rename from search.png rename to img/search.png diff --git a/url-parsing.png b/img/url-parsing.png similarity index 100% rename from url-parsing.png rename to img/url-parsing.png From e5932b1eb165919f1601636bd9e7c7a28efaa609 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 11:17:29 +0000 Subject: [PATCH 03/32] Renamed getFrontMatterLocation to getFrontMatterCoordinate The old function name read like it would return the location of the front matter within the file. The new function name better describes what it does which is getting the coordinate from the front matter --- src/main.ts | 6 +++--- src/markers.ts | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main.ts b/src/main.ts index 0bb257a..773a4a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import { UrlConvertor } from 'src/urlConvertor'; import { MapView } from 'src/mapView'; import { PluginSettings, DEFAULT_SETTINGS, convertLegacyMarkerIcons, convertLegacyTilesUrl } from 'src/settings'; -import { getFrontMatterLocation, matchInlineLocation, verifyLocation } from 'src/markers'; +import { getFrontMatterCoordinate, matchInlineLocation, verifyLocation } from 'src/markers'; import { SettingsTab } from 'src/settingsTab'; import { NewNoteDialog } from 'src/newNoteDialog'; import * as utils from 'src/utils'; @@ -94,7 +94,7 @@ export default class MapViewPlugin extends Plugin { this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile, _source: string, leaf?: WorkspaceLeaf) => { if (file instanceof TFile) { - const location = getFrontMatterLocation(file, this.app); + const location = getFrontMatterCoordinate(file, this.app); if (location) { menu.addItem((item: MenuItem) => { item.setTitle('Show on map'); @@ -201,7 +201,7 @@ export default class MapViewPlugin extends Plugin { selectedLocation = new leaflet.LatLng(parseFloat(match[2]), parseFloat(match[3])); else { - const fmLocation = getFrontMatterLocation(view.file, this.app); + const fmLocation = getFrontMatterCoordinate(view.file, this.app); if (line.indexOf('location') > -1 && fmLocation) selectedLocation = fmLocation; } diff --git a/src/markers.ts b/src/markers.ts index d5097ca..8f3e4e2 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -57,7 +57,7 @@ export async function buildAndAppendFileMarkers(mapToAppendTo: FileMarker[], fil const frontMatter = fileCache?.frontmatter; if (frontMatter) { if (!skipMetadata) { - const location = getFrontMatterLocation(file, app); + const location = getFrontMatterCoordinate(file, app); if (location) { verifyLocation(location); let leafletMarker = new FileMarker(file, location); @@ -210,7 +210,12 @@ async function makeTextSnippet(file: TFile, fileContent: string, fileLocation: n return snippet; } -export function getFrontMatterLocation(file: TFile, app: App) : leaflet.LatLng { +/** + * Get the coordinates stored in the front matter of a file + * @param file The file to load the front matter from + * @param app The app to load the file from + */ +export function getFrontMatterCoordinate(file: TFile, app: App) : leaflet.LatLng { const fileCache = app.metadataCache.getFileCache(file); const frontMatter = fileCache?.frontmatter; if (frontMatter && frontMatter?.location) { From 416aacf54e4f0d91797b376a638efe9a86442aff Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 11:59:20 +0000 Subject: [PATCH 04/32] Renamed another two methods and variables Two more functions were a little ambiguous so renamed them to be less ambiguous Renamed location variable to coordinate --- src/main.ts | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/main.ts b/src/main.ts index 773a4a9..6fc51e8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -94,20 +94,19 @@ export default class MapViewPlugin extends Plugin { this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile, _source: string, leaf?: WorkspaceLeaf) => { if (file instanceof TFile) { - const location = getFrontMatterCoordinate(file, this.app); - if (location) { + const coordinate = getFrontMatterCoordinate(file, this.app); + if (coordinate) { menu.addItem((item: MenuItem) => { item.setTitle('Show on map'); item.setIcon('globe'); - item.onClick(async (evt: MouseEvent) => await this.openMapWithLocation(location, evt.ctrlKey)); + item.onClick(async (evt: MouseEvent) => await this.openMapAtCoordinate(coordinate, evt.ctrlKey)); }); menu.addItem((item: MenuItem) => { item.setTitle('Open with default app'); item.onClick(_ev => { - open(`geo:${location.lat},${location.lng}`); + open(`geo:${coordinate.lat},${coordinate.lng}`); }); }); - utils.populateOpenInItems(menu, location, this.settings); } else { if (leaf && leaf.view instanceof MarkdownView) { const editor = leaf.view.editor; @@ -118,6 +117,7 @@ export default class MapViewPlugin extends Plugin { const dialog = new NewNoteDialog(this.app, this.settings, 'addToNote', editor); dialog.open(); }); + utils.populateOpenInItems(menu, coordinate, this.settings); }); } @@ -127,20 +127,20 @@ export default class MapViewPlugin extends Plugin { this.app.workspace.on('editor-menu', async (menu: Menu, editor: Editor, view: MarkdownView) => { if (view instanceof FileView) { - const location = this.getLocationOnEditorLine(editor, view); - if (location) { + const coordinate = this.getEditorLineCoordinate(editor, view); + if (coordinate) { menu.addItem((item: MenuItem) => { item.setTitle('Show on map'); item.setIcon('globe'); - item.onClick(async (evt: MouseEvent) => await this.openMapWithLocation(location, evt.ctrlKey)); + item.onClick(async (evt: MouseEvent) => await this.openMapAtCoordinate(coordinate, evt.ctrlKey)); }); menu.addItem((item: MenuItem) => { item.setTitle('Open with default app'); item.onClick(_ev => { - open(`geo:${location.lat},${location.lng}`); + open(`geo:${coordinate.lat},${coordinate.lng}`); }); }); - utils.populateOpenInItems(menu, location, this.settings); + utils.populateOpenInItems(menu, coordinate, this.settings); } if (editor.getSelection()) { menu.addItem((item: MenuItem) => { @@ -172,7 +172,13 @@ export default class MapViewPlugin extends Plugin { } - private async openMapWithLocation(location: leaflet.LatLng, ctrlKey: boolean) { + /** + * Open an instance of the map at the given coordinate + * @param coordinate The coordinate to open the map at + * @param ctrlKey Was the control key pressed. If true will open a map in the current leaf rather than using an open map. + * @private + */ + private async openMapAtCoordinate(coordinate: leaflet.LatLng, ctrlKey: boolean) { // Find the best candidate for a leaf to open the map view on. // If there's an open map view, use that, otherwise use the current leaf. // If Ctrl is pressed, override that behavior and always use the current leaf. @@ -188,12 +194,19 @@ export default class MapViewPlugin extends Plugin { type: consts.MAP_VIEW_NAME, state: { version: this.highestVersionSeen + 1, // Make sure this overrides any existing state - mapCenter: location, + mapCenter: coordinate, mapZoom: this.settings.zoomOnGoFromNote - } as any}); + } as any + }); } - private getLocationOnEditorLine(editor: Editor, view: FileView): leaflet.LatLng { + /** + * Get the coordinate on the current editor line + * @param editor obsidian Editor instance + * @param view obsidian FileView instance + * @private + */ + private getEditorLineCoordinate(editor: Editor, view: FileView): leaflet.LatLng { const line = editor.getLine(editor.getCursor().line); const match = matchInlineLocation(line)[0]; let selectedLocation = null; From 440674289be0858e804f4944aa8aabf5acc6a7a8 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 12:03:20 +0000 Subject: [PATCH 05/32] Simplified if else --- src/main.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main.ts b/src/main.ts index 6fc51e8..4e354db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -107,20 +107,17 @@ export default class MapViewPlugin extends Plugin { open(`geo:${coordinate.lat},${coordinate.lng}`); }); }); - } else { - if (leaf && leaf.view instanceof MarkdownView) { - const editor = leaf.view.editor; - menu.addItem((item: MenuItem) => { - item.setTitle('Add geolocation (front matter)'); - item.setIcon('globe'); - item.onClick(async (evt: MouseEvent) => { - const dialog = new NewNoteDialog(this.app, this.settings, 'addToNote', editor); - dialog.open(); - }); utils.populateOpenInItems(menu, coordinate, this.settings); + } else if (leaf && leaf.view instanceof MarkdownView) { + const editor = leaf.view.editor; + menu.addItem((item: MenuItem) => { + item.setTitle('Add geolocation (front matter)'); + item.setIcon('globe'); + item.onClick(async (evt: MouseEvent) => { + const dialog = new NewNoteDialog(this.app, this.settings, 'addToNote', editor); + dialog.open(); }); - - } + }); } } }); From d176ce3ac8cc7a85527980c15ad7abd212893287 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 14:40:25 +0000 Subject: [PATCH 06/32] Renamed urlConverter to coordinateParser This class is capable of parsing more than just urls Also renamed some methods --- src/{urlConvertor.ts => coordinateParser.ts} | 24 +++++++++++++++----- src/main.ts | 15 ++++++------ src/newNoteDialog.ts | 8 +++---- 3 files changed, 29 insertions(+), 18 deletions(-) rename src/{urlConvertor.ts => coordinateParser.ts} (72%) diff --git a/src/urlConvertor.ts b/src/coordinateParser.ts similarity index 72% rename from src/urlConvertor.ts rename to src/coordinateParser.ts index a2f975f..4a5bf63 100644 --- a/src/urlConvertor.ts +++ b/src/coordinateParser.ts @@ -4,23 +4,35 @@ import * as leaflet from 'leaflet'; import { PluginSettings } from 'src/settings'; import * as utils from 'src/utils'; -export class UrlConvertor { +/** + * A class to convert a URL string into coordinates + */ +export class CoordinateParser { private settings: PluginSettings; constructor(app: App, settings: PluginSettings) { this.settings = settings; } - findMatchInLine(editor: Editor) { + /** + * Find a coordinate in the current editor line + * @param editor The obsidian Editor instance to use + */ + parseCoordinateFromLine(editor: Editor) { const cursor = editor.getCursor(); - const result = this.parseLocationFromUrl(editor.getLine(cursor.line)); + const result = this.parseCoordinateFromString(editor.getLine(cursor.line)); return result?.location; } - parseLocationFromUrl(line: string) { + /** + * Get coordinates from an encoded string (usually a URL). + * Will try each url parsing rule until one succeeds. + * @param str The string to decode + */ + parseCoordinateFromString(str: string) { for (const rule of this.settings.urlParsingRules) { const regexp = RegExp(rule.regExp, 'g'); - const results = line.matchAll(regexp); + const results = str.matchAll(regexp); for (let result of results) { try { return { @@ -52,7 +64,7 @@ export class UrlConvertor { convertUrlAtCursorToGeolocation(editor: Editor) { const cursor = editor.getCursor(); - const result = this.parseLocationFromUrl(editor.getLine(cursor.line)); + const result = this.parseCoordinateFromString(editor.getLine(cursor.line)); if (result) this.insertLocationToEditor(result.location, editor, {line: cursor.line, ch: result.index}, result.matchLength); } diff --git a/src/main.ts b/src/main.ts index 4e354db..9f7e920 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import { addIcon, Notice, Editor, FileView, MarkdownView, MenuItem, Menu, TFile, import * as consts from 'src/consts'; import * as leaflet from 'leaflet'; import { LocationSuggest } from 'src/geosearch'; -import { UrlConvertor } from 'src/urlConvertor'; +import { CoordinateParser } from 'src/coordinateParser'; import { MapView } from 'src/mapView'; import { PluginSettings, DEFAULT_SETTINGS, convertLegacyMarkerIcons, convertLegacyTilesUrl } from 'src/settings'; @@ -15,7 +15,7 @@ export default class MapViewPlugin extends Plugin { settings: PluginSettings; public highestVersionSeen: number = 0; private suggestor: LocationSuggest; - private urlConvertor: UrlConvertor; + private coordinateParser: CoordinateParser; async onload() { addIcon('globe', consts.RIBBON_ICON); @@ -31,7 +31,7 @@ export default class MapViewPlugin extends Plugin { }); this.suggestor = new LocationSuggest(this.app, this.settings); - this.urlConvertor = new UrlConvertor(this.app, this.settings); + this.coordinateParser = new CoordinateParser(this.app, this.settings); this.registerEditorSuggest(this.suggestor); @@ -146,27 +146,26 @@ export default class MapViewPlugin extends Plugin { }); } - if (this.urlConvertor.findMatchInLine(editor)) + if (this.coordinateParser.parseCoordinateFromLine(editor)) menu.addItem((item: MenuItem) => { item.setTitle('Convert to geolocation'); item.onClick(async () => { - this.urlConvertor.convertUrlAtCursorToGeolocation(editor); + this.coordinateParser.convertUrlAtCursorToGeolocation(editor); }); }) const clipboard = await navigator.clipboard.readText(); - const clipboardLocation = this.urlConvertor.parseLocationFromUrl(clipboard)?.location; + const clipboardLocation = this.coordinateParser.parseCoordinateFromString(clipboard)?.location; if (clipboardLocation) { menu.addItem((item: MenuItem) => { item.setTitle('Paste as geolocation'); item.onClick(async () => { - this.urlConvertor.insertLocationToEditor(clipboardLocation, editor); + this.coordinateParser.insertLocationToEditor(clipboardLocation, editor); }); }) } } }); - } /** diff --git a/src/newNoteDialog.ts b/src/newNoteDialog.ts index eeb14e8..8bfcdf6 100644 --- a/src/newNoteDialog.ts +++ b/src/newNoteDialog.ts @@ -3,7 +3,7 @@ import * as leaflet from 'leaflet'; import { PluginSettings } from 'src/settings'; import { LocationSuggest } from 'src/geosearch'; -import { UrlConvertor } from 'src/urlConvertor'; +import { CoordinateParser } from 'src/coordinateParser'; import { MapView } from 'src/mapView'; import * as utils from 'src/utils'; import * as consts from 'src/consts'; @@ -17,7 +17,7 @@ class SuggestInfo { export class NewNoteDialog extends SuggestModal { private settings: PluginSettings; private suggestor: LocationSuggest; - private urlConvertor: UrlConvertor; + private coordinateParser: CoordinateParser; private lastSearchTime = 0; private delayInMs = 250; private lastSearch = ''; @@ -30,7 +30,7 @@ export class NewNoteDialog extends SuggestModal { super(app); this.settings = settings; this.suggestor = new LocationSuggest(this.app, this.settings); - this.urlConvertor = new UrlConvertor(this.app, this.settings); + this.coordinateParser = new CoordinateParser(this.app, this.settings); this.dialogAction = dialogAction; this.editor = editor; @@ -112,7 +112,7 @@ export class NewNoteDialog extends SuggestModal { } parseLocationAsUrl(query: string): SuggestInfo { - const result = this.urlConvertor.parseLocationFromUrl(query); + const result = this.coordinateParser.parseCoordinateFromString(query); if (result) return { name: `Parsed from ${result.ruleName}: ${result.location.lat}, ${result.location.lng}`, From 698c6681ae0400d21dada33951f40228f866ef82 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 14:43:24 +0000 Subject: [PATCH 07/32] Added some comments to the main script --- src/main.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/main.ts b/src/main.ts index 9f7e920..5022654 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,9 @@ import { SettingsTab } from 'src/settingsTab'; import { NewNoteDialog } from 'src/newNoteDialog'; import * as utils from 'src/utils'; +/** + * A plugin to implement map support + */ export default class MapViewPlugin extends Plugin { settings: PluginSettings; public highestVersionSeen: number = 0; @@ -18,15 +21,23 @@ export default class MapViewPlugin extends Plugin { private coordinateParser: CoordinateParser; async onload() { + // When the plugin loads + + // Register a new icon addIcon('globe', consts.RIBBON_ICON); + // Load the settings await this.loadSettings(); + // Add a new ribbon entry to the left bar this.addRibbonIcon('globe', 'Open map view', () => { + // when clicked change the active view to the map this.app.workspace.getLeaf().setViewState({type: consts.MAP_VIEW_NAME}); }); + // Register a new viewer for maps this.registerView(consts.MAP_VIEW_NAME, (leaf: WorkspaceLeaf) => { + // Create a new map instance return new MapView(leaf, this.settings, this); }); @@ -35,6 +46,7 @@ export default class MapViewPlugin extends Plugin { this.registerEditorSuggest(this.suggestor); + // convert old data if (convertLegacyMarkerIcons(this.settings)) { await this.saveSettings(); new Notice("Map View: legacy marker icons were converted to the new format"); @@ -44,24 +56,31 @@ export default class MapViewPlugin extends Plugin { new Notice("Map View: legacy tiles URL was converted to the new format"); } + // Register commands to the command palette + // command that opens the map view (same as clicking the map icon) this.addCommand({ id: 'open-map-view', name: 'Open Map View', callback: () => { + // The command to run this.app.workspace.getLeaf().setViewState({type: consts.MAP_VIEW_NAME}); }, }); + // command that looks up the selected text to find the location this.addCommand({ id: 'convert-selection-to-location', name: 'Convert Selection to Geolocation', editorCheckCallback: (checking, editor, view) => { + // This is run once when building the command list and again when it is actually run + // In the former checking is true and in the latter it is false if (checking) return editor.getSelection().length > 0; this.suggestor.selectionToLink(editor); } }); + // command that adds a blank inline location at the cursor location this.addCommand({ id: 'insert-geolink', name: 'Add inline geolocation link', @@ -72,6 +91,7 @@ export default class MapViewPlugin extends Plugin { } }); + // command that opens the location search dialog and creates a new note from this location this.addCommand({ id: 'new-geolocation-note', name: 'New geolocation note', @@ -81,6 +101,7 @@ export default class MapViewPlugin extends Plugin { } }); + // command that opens the location search dialog and adds the location to the current note this.addCommand({ id: 'add-frontmatter-geolocation', name: 'Add geolocation (front matter) to current note', @@ -92,24 +113,32 @@ export default class MapViewPlugin extends Plugin { this.addSettingTab(new SettingsTab(this.app, this)); + // Modify the file context menu (run when the context menu is built) this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile, _source: string, leaf?: WorkspaceLeaf) => { if (file instanceof TFile) { const coordinate = getFrontMatterCoordinate(file, this.app); if (coordinate) { + // If there is a coordinate in the front matter of the file + // add an option to open it in the map menu.addItem((item: MenuItem) => { item.setTitle('Show on map'); item.setIcon('globe'); item.onClick(async (evt: MouseEvent) => await this.openMapAtCoordinate(coordinate, evt.ctrlKey)); }); + // add an option to open it in the default app menu.addItem((item: MenuItem) => { item.setTitle('Open with default app'); item.onClick(_ev => { open(`geo:${coordinate.lat},${coordinate.lng}`); }); }); + // populate user defined url formats utils.populateOpenInItems(menu, coordinate, this.settings); } else if (leaf && leaf.view instanceof MarkdownView) { + // If there is no valid location field in the file + // and the context menu came from the leaf (three dots in the top right) const editor = leaf.view.editor; + // add a menu item to create the front matter menu.addItem((item: MenuItem) => { item.setTitle('Add geolocation (front matter)'); item.setIcon('globe'); @@ -122,24 +151,30 @@ export default class MapViewPlugin extends Plugin { } }); + // Modify the editor context menu (run when the context menu is built) this.app.workspace.on('editor-menu', async (menu: Menu, editor: Editor, view: MarkdownView) => { if (view instanceof FileView) { const coordinate = this.getEditorLineCoordinate(editor, view); if (coordinate) { + // If there is a coordinate one the line + // add an option to open it in the map menu.addItem((item: MenuItem) => { item.setTitle('Show on map'); item.setIcon('globe'); item.onClick(async (evt: MouseEvent) => await this.openMapAtCoordinate(coordinate, evt.ctrlKey)); }); + // add an option to open it in the default app menu.addItem((item: MenuItem) => { item.setTitle('Open with default app'); item.onClick(_ev => { open(`geo:${coordinate.lat},${coordinate.lng}`); }); }); + // populate user defined url formats utils.populateOpenInItems(menu, coordinate, this.settings); } if (editor.getSelection()) { + // If there is text selected add a menu item to convert it to coordinates menu.addItem((item: MenuItem) => { item.setTitle('Convert to geolocation (geosearch)'); item.onClick(async () => await this.suggestor.selectionToLink(editor)); @@ -147,6 +182,7 @@ export default class MapViewPlugin extends Plugin { } if (this.coordinateParser.parseCoordinateFromLine(editor)) + // if the line contains a valid string coordinate menu.addItem((item: MenuItem) => { item.setTitle('Convert to geolocation'); item.onClick(async () => { @@ -157,6 +193,7 @@ export default class MapViewPlugin extends Plugin { const clipboard = await navigator.clipboard.readText(); const clipboardLocation = this.coordinateParser.parseCoordinateFromString(clipboard)?.location; if (clipboardLocation) { + // if the clipboard contains a valid string coordinate menu.addItem((item: MenuItem) => { item.setTitle('Paste as geolocation'); item.onClick(async () => { @@ -224,10 +261,16 @@ export default class MapViewPlugin extends Plugin { onunload() { } + /** + * Load the settings from disk + */ async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } + /** + * Save the settings to disk + */ async saveSettings() { await this.saveData(this.settings); } From 714102999a1fac88da2afc58c3b5b75c146d0c95 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 17:07:59 +0000 Subject: [PATCH 08/32] Cleaned up the coordinate parser Renamed a number of methods and added docstrings. parseCoordinateFromLine -> parseEditorLine parseCoordinateFromString -> parseString insertLocationToEditor -> editorInsertGeolocation convertUrlAtCursorToGeolocation -> editorLineToGeolocation --- src/coordinateParser.ts | 94 +++++++++++++++++++++++++++++------------ src/main.ts | 8 ++-- src/newNoteDialog.ts | 2 +- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/coordinateParser.ts b/src/coordinateParser.ts index 4a5bf63..1470452 100644 --- a/src/coordinateParser.ts +++ b/src/coordinateParser.ts @@ -4,32 +4,35 @@ import * as leaflet from 'leaflet'; import { PluginSettings } from 'src/settings'; import * as utils from 'src/utils'; + +interface FindResult { + location: leaflet.LatLng; + index: number, + matchLength: number, + ruleName: string +} + /** - * A class to convert a URL string into coordinates + * A class to convert a string (usually a URL) into coordinate format */ export class CoordinateParser { private settings: PluginSettings; - constructor(app: App, settings: PluginSettings) { - this.settings = settings; - } - /** - * Find a coordinate in the current editor line - * @param editor The obsidian Editor instance to use + * construct an instance of CoordinateParser + * @param app The obsidian App instance + * @param settings The plugin settings */ - parseCoordinateFromLine(editor: Editor) { - const cursor = editor.getCursor(); - const result = this.parseCoordinateFromString(editor.getLine(cursor.line)); - return result?.location; + constructor(app: App, settings: PluginSettings) { + this.settings = settings; } /** - * Get coordinates from an encoded string (usually a URL). + * Get coordinate from an encoded string (usually a URL). * Will try each url parsing rule until one succeeds. * @param str The string to decode */ - parseCoordinateFromString(str: string) { + parseString(str: string): FindResult | null { for (const rule of this.settings.urlParsingRules) { const regexp = RegExp(rule.regExp, 'g'); const results = str.matchAll(regexp); @@ -48,24 +51,61 @@ export class CoordinateParser { return null; } - insertLocationToEditor(location: leaflet.LatLng, editor: Editor, replaceStart?: EditorPosition, replaceLength?: number) { - const locationString = `[](geo:${location.lat},${location.lng})`; + /** + * Parse the line where the cursor is in the editor + * @param editor The obsidian Editor instance to use + */ + parseEditorLine(editor: Editor): FindResult | null { const cursor = editor.getCursor(); - if (replaceStart && replaceLength) { - editor.replaceRange(locationString, replaceStart, {line: replaceStart.line, ch: replaceStart.ch + replaceLength}); - } - else + const line = editor.getLine(cursor.line); + return this.parseString(line); + } + + /** + * Insert a geo link into the editor at the cursor position + * @param editor The obsidian Editor instance + * @param coordinate The coordinate to insert into the editor + * @param replaceStart The EditorPosition to start the replacement at. If null will replace any text selected + * @param replaceEnd The EditorPosition to stop the replacement at. If null will replace any text selected + */ + editorInsertGeolocation(editor: Editor, coordinate: leaflet.LatLng, replaceStart?: EditorPosition, replaceEnd?: EditorPosition) { + const locationString = `[](geo:${coordinate.lat},${coordinate.lng})`; + let newCursorPos: EditorPosition; + if (replaceStart && replaceEnd) { + editor.replaceRange(locationString, replaceStart, replaceEnd); + newCursorPos = {line: replaceStart.line, ch: replaceStart.ch + 1}; + } else { + const cursor = editor.getCursor(); editor.replaceSelection(locationString); - // We want to put the cursor right after the beginning of the newly-inserted link - const newCursorPos = replaceStart ? replaceStart.ch + 1 : cursor.ch + 1; - editor.setCursor({line: cursor.line, ch: newCursorPos}); + newCursorPos = {line: cursor.line, ch: cursor.ch + 1}; + } + + // Put the cursor after the opening square bracket + editor.setCursor(newCursorPos); + // TODO: This will modify in a second operation. + // Is there a way to make it apply in one operation so that one undo undoes both changes utils.verifyOrAddFrontMatter(editor, 'locations', ''); } - convertUrlAtCursorToGeolocation(editor: Editor) { - const cursor = editor.getCursor(); - const result = this.parseCoordinateFromString(editor.getLine(cursor.line)); - if (result) - this.insertLocationToEditor(result.location, editor, {line: cursor.line, ch: result.index}, result.matchLength); + /** + * Replace the text at the cursor location with a geo link + * @param editor The obsidian Editor instance + */ + editorLineToGeolocation(editor: Editor): void { + const result = this.parseEditorLine(editor) + if (result) { + this.editorInsertGeolocation( + editor, + result.location, + { + line: editor.getCursor().line, + ch: result.index + }, + { + line: editor.getCursor().line, + ch: result.index + result.matchLength + } + ); + } } } diff --git a/src/main.ts b/src/main.ts index 5022654..b636736 100644 --- a/src/main.ts +++ b/src/main.ts @@ -181,23 +181,23 @@ export default class MapViewPlugin extends Plugin { }); } - if (this.coordinateParser.parseCoordinateFromLine(editor)) + if (this.coordinateParser.parseEditorLine(editor)) // if the line contains a valid string coordinate menu.addItem((item: MenuItem) => { item.setTitle('Convert to geolocation'); item.onClick(async () => { - this.coordinateParser.convertUrlAtCursorToGeolocation(editor); + this.coordinateParser.editorLineToGeolocation(editor); }); }) const clipboard = await navigator.clipboard.readText(); - const clipboardLocation = this.coordinateParser.parseCoordinateFromString(clipboard)?.location; + const clipboardLocation = this.coordinateParser.parseString(clipboard)?.location; if (clipboardLocation) { // if the clipboard contains a valid string coordinate menu.addItem((item: MenuItem) => { item.setTitle('Paste as geolocation'); item.onClick(async () => { - this.coordinateParser.insertLocationToEditor(clipboardLocation, editor); + this.coordinateParser.editorInsertGeolocation(editor, clipboardLocation); }); }) } diff --git a/src/newNoteDialog.ts b/src/newNoteDialog.ts index 8bfcdf6..843c361 100644 --- a/src/newNoteDialog.ts +++ b/src/newNoteDialog.ts @@ -112,7 +112,7 @@ export class NewNoteDialog extends SuggestModal { } parseLocationAsUrl(query: string): SuggestInfo { - const result = this.coordinateParser.parseCoordinateFromString(query); + const result = this.coordinateParser.parseString(query); if (result) return { name: `Parsed from ${result.ruleName}: ${result.location.lat}, ${result.location.lng}`, From 91b2e54945069a885156d462188610a871c5b801 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 7 Feb 2022 19:23:13 +0000 Subject: [PATCH 09/32] Renamed the globe icon --- src/consts.ts | 2 +- src/main.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/consts.ts b/src/consts.ts index e1fefd7..ae65f11 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -3,7 +3,7 @@ import * as leaflet from 'leaflet'; export const MAP_VIEW_NAME = 'map'; // SVG editor used: https://svgedit.netlify.app/editor/index.html -export const RIBBON_ICON = ''; +export const GLOBE_ICON = ''; export const TILES_URL_OPENSTREETMAP = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; export const SEARCH_RESULT_MARKER = {prefix: 'fas', icon: 'fa-search', markerColor: 'blue'}; diff --git a/src/main.ts b/src/main.ts index b636736..a2aa3c4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,7 +24,7 @@ export default class MapViewPlugin extends Plugin { // When the plugin loads // Register a new icon - addIcon('globe', consts.RIBBON_ICON); + addIcon('globe', consts.GLOBE_ICON); // Load the settings await this.loadSettings(); From 332dffa9b7f78487848d0b5115ca94c6811df35d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 8 Feb 2022 10:58:15 +0000 Subject: [PATCH 10/32] Split verifyOrAddFrontMatter in two Split the yaml parsing and overwriting logic into a new function with a callback to do the modifying. This handles the yaml properly by parsing and then serialising it back. The previous implementation broke if the key exists only as a nested key. The value entry is now optional (defaulting to null) The value entry is now any arbitrary value which is then serialised to yaml. The only negative of this is that it populates null if no value is defined in the yaml --- src/coordinateParser.ts | 2 +- src/geosearch.ts | 4 +- src/newNoteDialog.ts | 6 +-- src/utils.ts | 107 +++++++++++++++++++++++++++++++--------- 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/coordinateParser.ts b/src/coordinateParser.ts index 1470452..d2bbf16 100644 --- a/src/coordinateParser.ts +++ b/src/coordinateParser.ts @@ -84,7 +84,7 @@ export class CoordinateParser { editor.setCursor(newCursorPos); // TODO: This will modify in a second operation. // Is there a way to make it apply in one operation so that one undo undoes both changes - utils.verifyOrAddFrontMatter(editor, 'locations', ''); + utils.frontMatterSetDefault(editor, 'locations'); } /** diff --git a/src/geosearch.ts b/src/geosearch.ts index 6c1870d..771abe0 100644 --- a/src/geosearch.ts +++ b/src/geosearch.ts @@ -58,7 +58,7 @@ export class LocationSuggest extends EditorSuggest { finalResult, {line: currentCursor.line, ch: linkOfCursor.index}, {line: currentCursor.line, ch: linkOfCursor.linkEnd}); - if (utils.verifyOrAddFrontMatter(value.context.editor, 'locations', '')) + if (utils.frontMatterSetDefault(value.context.editor, 'locations')) new Notice("The note's front matter was updated to denote locations are present"); } @@ -100,7 +100,7 @@ export class LocationSuggest extends EditorSuggest { const firstResult = results[0]; editor.replaceSelection(`[${selection}](geo:${firstResult.y},${firstResult.x})`); new Notice(firstResult.label, 10 * 1000); - if (utils.verifyOrAddFrontMatter(editor, 'locations', '')) + if (utils.frontMatterSetDefault(editor, 'locations')) new Notice("The note's front matter was updated to denote locations are present"); } else { diff --git a/src/newNoteDialog.ts b/src/newNoteDialog.ts index 843c361..b69a80f 100644 --- a/src/newNoteDialog.ts +++ b/src/newNoteDialog.ts @@ -81,9 +81,9 @@ export class NewNoteDialog extends SuggestModal { } } - async addToNote(location: leaflet.LatLng, ev: MouseEvent | KeyboardEvent, query: string) { - const locationString = `[${location.lat},${location.lng}]`; - utils.verifyOrAddFrontMatter(this.editor, 'location', locationString); + async addToNote(coordinate: leaflet.LatLng, ev: MouseEvent | KeyboardEvent, query: string) { + const coordianteArray = [coordinate.lat,coordinate.lng]; + utils.frontMatterSetDefault(this.editor, 'location', coordianteArray); } async getSearchResultsWithDelay(query: string) { diff --git a/src/utils.ts b/src/utils.ts index 8c84d2c..edc67c2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { WorkspaceLeaf, MarkdownView, Editor, App, TFile, Menu, MenuItem } from 'obsidian'; +import {WorkspaceLeaf, MarkdownView, Editor, App, TFile, Menu, MenuItem, stringifyYaml, parseYaml, Notice} from 'obsidian'; import * as moment_ from 'moment'; import * as leaflet from 'leaflet'; @@ -66,36 +66,97 @@ export async function handleNewNoteCursorMarker(editor: Editor) { } } -// Creates or modifies a front matter that has the field `fieldName: fieldValue`. -// Returns true if a change to the note was made. -export function verifyOrAddFrontMatter(editor: Editor, fieldName: string, fieldValue: string): boolean { +/** + * A utility for reading, modifying and writing back front matter. + * Load front matter from the current file and pass it to edit_callback. + * The returned value from the callback replaces the original front matter + * Return true if the yaml is changed + * @param editor The obsidian editor instance + * @param edit_callback The callback to modify the parsed object. Must return the modified object or null if no changes were made. + */ +export function updateFrontMatter( + editor: Editor, + edit_callback: (frontMatter: any) => any | null +){ const content = editor.getValue(); - const frontMatterRegex = /^---(.*)^---/ms; - const frontMatter = content.match(frontMatterRegex); - const existingFieldRegex = new RegExp(`^---.*${fieldName}:.*^---`, 'ms'); - const existingField = content.match(existingFieldRegex); - const cursorLocation = editor.getCursor(); - // That's not the best usage of the API, and rather be converted to editor transactions or something else - // that can preserve the cursor position better - if (frontMatter && !existingField) { - const replaced = `---${frontMatter[1]}${fieldName}: ${fieldValue}\n---`; - editor.setValue(content.replace(frontMatterRegex, replaced)); - editor.setCursor({line: cursorLocation.line + 1, ch: cursorLocation.ch}); - return true; - } else if (!frontMatter) { - const newFrontMatter = `---\n${fieldName}: ${fieldValue}\n---\n\n`; - editor.setValue(newFrontMatter + content); - editor.setCursor({line: cursorLocation.line + newFrontMatter.split('\n').length - 1, ch: cursorLocation.ch}); - return true; + const frontMatterRegex = /^---(.*?)\n---/s; + const frontMatterMatch = content.match(frontMatterRegex); + let frontMatter: any + if (frontMatterMatch){ + // found valid front matter + const frontMatterYaml = frontMatterMatch[1]; + try { + // parse the front matter into an object for easier handling + frontMatter = parseYaml(frontMatterYaml); + } catch (error) { + new Notice("Could not parse front matter yaml.\n" + error); + return false + } + if (typeof frontMatter === "object") { + frontMatter = edit_callback(frontMatter); + if (frontMatter) { + const newFrontMatter = `---\n${stringifyYaml(frontMatter)}\n---`; + const cursorLocation = editor.getCursor(); + editor.setValue(content.replace(frontMatterRegex, newFrontMatter)); + editor.setCursor({line: cursorLocation.line + 1, ch: cursorLocation.ch}); + return true; + } + } else { + new Notice("Expected yaml front matter root to be an object/dictionary."); + } + } else { + // did not find valid front matter + frontMatter = {}; + frontMatter = edit_callback(frontMatter); + if (frontMatter) { + const newFrontMatter = `---\n${stringifyYaml(frontMatter)}\n---\n\n`; + const cursorLocation = editor.getCursor(); + editor.setValue(newFrontMatter + content); + editor.setCursor({line: cursorLocation.line + newFrontMatter.split('\n').length - 1, ch: cursorLocation.ch}); + return true; + } } return false; } -export function populateOpenInItems(menu: Menu, location: leaflet.LatLng, settings: settings.PluginSettings) { +/** + * Create or modify a front matter that has the field `fieldName: fieldValue`. + * Returns true if a change to the note was made. + * @param editor The obsidian Editor instance + * @param key_name The key to set in the yaml front matter + * @param default_value The default value to populate with if not defined. Defaults to null. + */ +export function frontMatterSetDefault(editor: Editor, key_name: string, default_value?: any): boolean { + return updateFrontMatter( + editor, + (frontMatter) => { + if (frontMatter.hasOwnProperty(key_name)) { + // if the front matter already has this key + if (frontMatter[key_name] === null && default_value !== null) { + // if the key exists but the value is null + frontMatter[key_name] = default_value + return frontMatter + } + return null; + } else { + frontMatter[key_name] = default_value + return frontMatter + } + } + ) +} + +/** + * Populate a context menu from the user configurable urls + * @param menu The menu to attach + * @param coordinate The coordinate to use in the menu item + * @param settings Plugin settings + */ +export function populateOpenInItems(menu: Menu, coordinate: leaflet.LatLng, settings: settings.PluginSettings) { for (let setting of settings.openIn) { if (!setting.name || !setting.urlPattern) continue; - const fullUrl = setting.urlPattern.replace('{x}', location.lat.toString()).replace('{y}', location.lng.toString()); + const fullUrl = setting.urlPattern.replace('{x}', coordinate.lat.toString()).replace('{y}', coordinate.lng.toString()); menu.addItem((item: MenuItem) => { item.setTitle(`Open in ${setting.name}`); item.onClick(_ev => { From d5d9df435f74754b3c0e6be389c3d09b5f8e3e88 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Tue, 8 Feb 2022 10:59:01 +0000 Subject: [PATCH 11/32] Renamed variable fileLocation to characterIndex --- src/utils.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index edc67c2..9bc2ffe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -43,9 +43,15 @@ export async function newNote(app: App, newNoteType: NewNoteType, directory: str } } -export async function goToEditorLocation(editor: Editor, fileLocation: number, highlight: boolean) { - if (fileLocation) { - let pos = editor.offsetToPos(fileLocation); +/** + * Go to a character index in the note + * @param editor The obsidian Editor instance + * @param characterIndex The character index in the file to go to + * @param highlight If true will select the whole line + */ +export async function goToEditorLocation(editor: Editor, characterIndex: number, highlight: boolean) { + if (characterIndex) { + let pos = editor.offsetToPos(characterIndex); if (highlight) { const lineContent = editor.getLine(pos.line); editor.setSelection({ch: 0, line: pos.line}, {ch: lineContent.length, line: pos.line}); From ed795f4b5f88f4eec49e160f9f9d9fdca0418014 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 11 Feb 2022 19:45:42 +0000 Subject: [PATCH 12/32] Cleaned up the newNote util function --- src/utils.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 9bc2ffe..e37b2b6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,21 +22,35 @@ type NewNoteType = 'singleLocation' | 'multiLocation'; const CURSOR = '$CURSOR$'; -export async function newNote(app: App, newNoteType: NewNoteType, directory: string, fileName: string, - location: string, templatePath?: string): Promise +/** + * Create a new markdown note and populate with the location + * @param app The obsidian app instance + * @param newNoteType The location format to encode as + * @param directory The directory path to put the file in + * @param fileName The name of the file + * @param coordinate The coordinate + * @param templatePath Optional path to a file to populate the file with + */ +export async function newNote( + app: App, + newNoteType: NewNoteType, + directory: string, + fileName: string, + coordinate: string, + templatePath?: string +): Promise { // `$CURSOR$` is used to set the cursor let content = newNoteType === 'singleLocation' ? - `---\nlocation: [${location}]\n---\n\n${CURSOR}` : - `---\nlocations:\n---\n\n\[${CURSOR}](geo:${location})\n`; - let templateContent = ''; + `---\nlocation: [${coordinate}]\n---\n\n${CURSOR}` : + `---\nlocations:\n---\n\n\[${CURSOR}](geo:${coordinate})\n`; if (templatePath) - templateContent = await app.vault.adapter.read(templatePath); + content = content + await app.vault.adapter.read(templatePath); let fullName = path.join(directory || '', fileName); if (await app.vault.adapter.exists(fullName + '.md')) fullName += Math.random() * 1000; try { - return app.vault.create(fullName + '.md', content + templateContent); + return app.vault.create(fullName + '.md', content); } catch (e) { throw Error(`Cannot create file named ${fullName}: ${e}`); From bc6db9351c1ef8486d5f2a1cd368f32633e73e0c Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 11 Feb 2022 19:56:33 +0000 Subject: [PATCH 13/32] Added docstrings and cleanup to mapView --- src/mapView.ts | 195 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 169 insertions(+), 26 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 1e226d4..94e6a45 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -1,11 +1,11 @@ -import { App, TAbstractFile, Editor, ButtonComponent, MarkdownView, getAllTags, ItemView, MenuItem, Menu, TFile, TextComponent, DropdownComponent, WorkspaceLeaf } from 'obsidian'; +import { TAbstractFile, Editor, ButtonComponent, getAllTags, ItemView, MenuItem, Menu, TFile, TextComponent, DropdownComponent, WorkspaceLeaf } from 'obsidian'; import * as leaflet from 'leaflet'; // Ugly hack for obsidian-leaflet compatability, see https://github.com/esm7/obsidian-map-view/issues/6 // @ts-ignore import * as leafletFullscreen from 'leaflet-fullscreen'; import '@fortawesome/fontawesome-free/js/all.min'; import 'leaflet/dist/leaflet.css'; -import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch'; +import { GeoSearchControl } from 'leaflet-geosearch'; import 'leaflet-geosearch/dist/geosearch.css'; import 'leaflet.markercluster/dist/MarkerCluster.css'; import 'leaflet.markercluster/dist/MarkerCluster.Default.css'; @@ -18,35 +18,101 @@ import { LocationSuggest } from 'src/geosearch'; import MapViewPlugin from 'src/main'; import * as utils from 'src/utils'; +/** + * The state of the map instance + */ type MapState = { + /** + * The zoom level of the map + */ mapZoom: number; + /** + * The viewed center of the map + */ mapCenter: leaflet.LatLng; + /** + * The tags that the user specified + */ tags: string[]; + /** + * The version of the map to track if data is old + */ version: number; -} +} +/** + * The map viewer class + */ export class MapView extends ItemView { private settings: PluginSettings; // The private state needs to be updated solely via updateMapToState private state: MapState; + /** + * The map data + * @private + */ private display = new class { + /** + * The leaflet map instance for this map viewer + */ map: leaflet.Map; + /** + * The cluster management class + */ clusterGroup: leaflet.MarkerClusterGroup; + /** + * The markers currently on the map + */ markers: MarkersMap = new Map(); + /** + * The HTML element containing the map controls + */ controlsDiv: HTMLDivElement; + /** + * The HTML element holding the map + */ mapDiv: HTMLDivElement; + /** + * The HTML text entry the user can type tags in + */ tagsBox: TextComponent; }; + /** + * The plugin instance + * @private + */ private plugin: MapViewPlugin; + /** + * The map state + * @private + */ private defaultState: MapState; + /** + * The leaf (obsidian sub-window) that a note was last opened in. + * This is cached so that it can be reused when opening notes in the future + * @private + */ private newPaneLeaf: WorkspaceLeaf; + /** + * Has the view been opened + * @private + */ private isOpen: boolean = false; + /** + * TODO: unused what does this do? + */ public onAfterOpen: (map: leaflet.Map, markers: MarkersMap) => any = null; + /** + * Construct a new map instance + * @param leaf The leaf the map should be put in + * @param settings The plugin settings + * @param plugin The plugin instance + */ constructor(leaf: WorkspaceLeaf, settings: PluginSettings, plugin: MapViewPlugin) { super(leaf); - this.navigation = true; + this.navigation = true; // TODO: unused? this.settings = settings; this.plugin = plugin; // Create the default state by the configuration @@ -73,18 +139,39 @@ export class MapView extends ItemView { return this.state; } - this.app.vault.on('delete', file => this.updateMarkersWithRelationToFile(file.path, null, true)); - this.app.vault.on('rename', (file, oldPath) => this.updateMarkersWithRelationToFile(oldPath, file, true)); - this.app.metadataCache.on('changed', file => this.updateMarkersWithRelationToFile(file.path, file, false)); + // Listen to file changes so we can rebuild the UI + this.app.vault.on( + 'delete', + file => this.onFileChange(file.path, null, true) + ); + this.app.vault.on( + 'rename', + (file, oldPath) => this.onFileChange(oldPath, file, true) + ); + this.app.metadataCache.on( + 'changed', + file => this.onFileChange(file.path, file, false) + ); this.app.workspace.on('css-change', () => { console.log('Map view: map refresh due to CSS change'); this.refreshMap(); }); } - getViewType() { return 'map'; } + /** + * The name of the view type + */ + getViewType() { return consts.MAP_VIEW_NAME; } + + /** + * The display name for the view + */ getDisplayText() { return 'Interactive Map View'; } + /** + * Is the map in dark mode + * @param settings + */ isDarkMode(settings: PluginSettings): boolean { if (settings.chosenMapMode === 'dark') return true; @@ -98,6 +185,9 @@ export class MapView extends ItemView { public updateMapSources = () => {}; + /** + * Run when the view is opened + */ async onOpen() { this.isOpen = true; this.state = this.defaultState; @@ -117,7 +207,6 @@ export class MapView extends ItemView { } createControls() { - var that = this; let controlsDiv = createDiv({ 'cls': 'graph-controls' }); @@ -136,7 +225,7 @@ export class MapView extends ItemView { this.display.tagsBox = new TextComponent(filtersContent); this.display.tagsBox.setPlaceholder('Tags, e.g. "#one,#two"'); this.display.tagsBox.onChange(async (tagsBox: string) => { - that.state.tags = tagsBox.split(',').filter(t => t.length > 0); + this.state.tags = tagsBox.split(',').filter(t => t.length > 0); await this.updateMapToState(this.state, this.settings.autoZoom); }); let tagSuggestions = new DropdownComponent(filtersContent); @@ -219,18 +308,27 @@ export class MapView extends ItemView { return controlsDiv; } + /** + * On view close + */ onClose() { this.isOpen = false; return super.onClose(); } + /** + * On window resize + */ onResize() { this.display.map.invalidateSize(); } async refreshMap() { + // remove all event listeners this.display?.map?.off(); + // destroy the map and event listeners this.display?.map?.remove(); + // clear the marker storage this.display?.markers?.clear(); this.display?.controlsDiv.remove(); this.display.controlsDiv = this.createControls(); @@ -239,16 +337,19 @@ export class MapView extends ItemView { } async createMap() { - var that = this; const isDark = this.isDarkMode(this.settings); // LeafletJS compatability: disable tree-shaking for the full-screen module var dummy = leafletFullscreen; - this.display.map = new leaflet.Map(this.display.mapDiv, { - center: this.defaultState.mapCenter, - zoom: this.defaultState.mapZoom, - zoomControl: false, - worldCopyJump: true, - maxBoundsViscosity: 1.0}); + this.display.map = new leaflet.Map( + this.display.mapDiv, + { + center: this.defaultState.mapCenter, + zoom: this.defaultState.mapZoom, + zoomControl: false, + worldCopyJump: true, + maxBoundsViscosity: 1.0 + } + ); leaflet.control.zoom({ position: 'topright' }).addTo(this.display.map); @@ -270,7 +371,8 @@ export class MapView extends ItemView { className: revertMap ? "dark-mode" : "" })); this.display.clusterGroup = new leaflet.MarkerClusterGroup({ - maxClusterRadius: this.settings.maxClusterRadiusPixels ?? DEFAULT_SETTINGS.maxClusterRadiusPixels}); + maxClusterRadius: this.settings.maxClusterRadiusPixels ?? DEFAULT_SETTINGS.maxClusterRadiusPixels + }); this.display.map.addLayer(this.display.clusterGroup); const suggestor = new LocationSuggest(this.app, this.settings); const searchControl = GeoSearchControl({ @@ -353,8 +455,13 @@ export class MapView extends ItemView { }); } - // Updates the map to the given state and then sets the state accordingly, but only if the given state version - // is not lower than the current state version (so concurrent async updates always keep the latest one) + /** + * Updates the map to the given state and then sets the state accordingly, but only if the given state version + * is not lower than the current state version (so concurrent async updates always keep the latest one) + * @param state + * @param autoFit + * @param force + */ async updateMapToState(state: MapState, autoFit: boolean = false, force: boolean = false) { if (this.settings.debug) console.time('updateMapToState'); @@ -380,6 +487,10 @@ export class MapView extends ItemView { console.timeEnd('updateMapToState'); } + /** + * Get a list of files containing at least one of the tags + * @param tags A list of string tags to match + */ getFileListByQuery(tags: string[]): TFile[] { let results: TFile[] = []; const allFiles = this.app.vault.getFiles(); @@ -401,6 +512,10 @@ export class MapView extends ItemView { return results; } + /** + * Remove markers that have changed or been removed and add the new ones + * @param newMarkers The new array of FileMarkers + */ updateMapMarkers(newMarkers: FileMarker[]) { let newMarkersMap: MarkersMap = new Map(); let markersToAdd: leaflet.Marker[] = []; @@ -465,6 +580,9 @@ export class MapView extends ItemView { return newMarker; } + /** + * Zoom the map to fit all markers on the screen + */ async autoFitMapToMarkers() { if (this.display.markers.size > 0) { const locations: leaflet.LatLng[] = Array.from(this.display.markers.values()).map(fileMarker => fileMarker.location); @@ -473,6 +591,12 @@ export class MapView extends ItemView { } } + /** + * Open a file in an editor window + * @param file The file descriptor to open + * @param useCtrlKeyBehavior If true will open the file in a different editor instance + * @param editorAction Optional callback to run when the file is opened + */ async goToFile(file: TFile, useCtrlKeyBehavior: boolean, editorAction?: (editor: Editor) => Promise) { let leafToUse = this.app.workspace.activeLeaf; const defaultDifferentPane = this.settings.markerClickBehavior != 'samePane'; @@ -510,11 +634,23 @@ export class MapView extends ItemView { await editorAction(editor); } + /** + * Open the marker in an editor instance + * @param marker The file marker to open + * @param useCtrlKeyBehavior If true the file will be opened in a new instance + * @param highlight If true will highlight the line + */ async goToMarker(marker: FileMarker, useCtrlKeyBehavior: boolean, highlight: boolean) { - return this.goToFile(marker.file, useCtrlKeyBehavior, - async (editor) => { await utils.goToEditorLocation(editor, marker.fileLocation, highlight); }); + return this.goToFile( + marker.file, + useCtrlKeyBehavior, + async (editor) => { await utils.goToEditorLocation(editor, marker.fileLocation, highlight); } + ); } + /** + * Get a list of all tags in the archive + */ getAllTagNames() : string[] { let tags: string[] = []; const allFiles = this.app.vault.getFiles(); @@ -529,9 +665,18 @@ export class MapView extends ItemView { return tags; } - private async updateMarkersWithRelationToFile(fileRemoved: string, fileAddedOrChanged: TAbstractFile, skipMetadata: boolean) { - if (!this.display.map || !this.isOpen) + /** + * Run when a file is deleted, renamed or changed + * @param fileRemoved The old file path + * @param fileAddedOrChanged The new file data + * @param skipMetadata currently unused TODO: what is this for? + * @private + */ + private async onFileChange(fileRemoved: string, fileAddedOrChanged: TAbstractFile, skipMetadata: boolean): Promise { + if (!this.display.map || !this.isOpen) { + // If the map has not been set up yet then do nothing return; + } let newMarkers: FileMarker[] = []; for (let [markerId, fileMarker] of this.display.markers) { if (fileMarker.file.path !== fileRemoved) @@ -541,6 +686,4 @@ export class MapView extends ItemView { await buildAndAppendFileMarkers(newMarkers, fileAddedOrChanged, this.settings, this.app) this.updateMapMarkers(newMarkers); } - } - From b6a2812ddf50e2289ea05b70689aa174570f98ec Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 11 Feb 2022 19:57:40 +0000 Subject: [PATCH 14/32] Added some docstrings to marker.ts --- src/markers.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/markers.ts b/src/markers.ts index 8f3e4e2..cdc6922 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -23,12 +23,21 @@ export class FileMarker { extraName?: string; tags: string[] = []; + /** + * Construct a new map pin object + * @param file + * @param location + */ constructor(file: TFile, location: leaflet.LatLng) { this.file = file; this.location = location; this.id = this.generateId(); } + /** + * Is the other data equal to this + * @param other A FileMarker to compare to + */ isSame(other: FileMarker) { return this.file.name === other.file.name && this.location.toString() === other.location.toString() && @@ -52,6 +61,14 @@ export class FileMarker { export type MarkersMap = Map; +/** + * Create file markers for every coordinate in the front matter and file body + * @param mapToAppendTo The list of file markers to append to + * @param file The file descriptor to parse + * @param settings The plugin settings + * @param app The obsidian app instance + * @param skipMetadata If true will not find markers in the front matter + */ export async function buildAndAppendFileMarkers(mapToAppendTo: FileMarker[], file: TFile, settings: PluginSettings, app: App, skipMetadata?: boolean) { const fileCache = app.metadataCache.getFileCache(file); const frontMatter = fileCache?.frontmatter; @@ -72,6 +89,12 @@ export async function buildAndAppendFileMarkers(mapToAppendTo: FileMarker[], fil } } +/** + * Create file marker instances for all the files in the vault + * @param files + * @param settings + * @param app + */ export async function buildMarkers(files: TFile[], settings: PluginSettings, app: App): Promise { if (settings.debug) console.time('buildMarkers'); @@ -89,6 +112,12 @@ function checkTagPatternMatch(tagPattern: string, tags: string[]) { return match && match.length > 0; } +/** + * Create a leaflet icon for the marker + * @param marker The file marker to create the icon for + * @param settings The plugin settings + * @param app The obsidian app instance + */ function getIconForMarker(marker: FileMarker, settings: PluginSettings, app: App) : leaflet.Icon { const fileCache = app.metadataCache.getFileCache(marker.file); // Combine the file tags with the marker-specific tags @@ -122,6 +151,11 @@ export function getIconFromOptions(iconSpec: leaflet.BaseIconOptions) : leaflet. } } +/** + * Make sure that the coordinates are valid world coordinates + * -90 <= longitude <= 90 and -180 <= latitude <= 180 + * @param location + */ export function verifyLocation(location: leaflet.LatLng) { if (location.lng < consts.LNG_LIMITS[0] || location.lng > consts.LNG_LIMITS[1]) throw Error(`Lng ${location.lng} is outside the allowed limits`); @@ -129,6 +163,10 @@ export function verifyLocation(location: leaflet.LatLng) { throw Error(`Lat ${location.lat} is outside the allowed limits`); } +/** + * Find all inline coordinates in a string + * @param content The file contents to find the coordinates in + */ export function matchInlineLocation(content: string): RegExpMatchArray[] { // Old syntax of ` `location: ... ` `. This syntax doesn't support a name so we leave an empty capture group const locationRegex1 = /\`()location:\s*\[?([0-9.\-]+)\s*,\s*([0-9.\-]+)\]?\`/g; @@ -139,6 +177,12 @@ export function matchInlineLocation(content: string): RegExpMatchArray[] { return Array.from(matches1).concat(Array.from(matches2)); } +/** + * Get markers from within the file body + * @param file The file descriptor to load + * @param settings The plugin settings + * @param app The obsidian app instance + */ async function getMarkersFromFileContent(file: TFile, settings: PluginSettings, app: App): Promise { let markers: FileMarker[] = []; const content = await app.vault.read(file); From d97a0420a37b8687144e13850b4cd40bf058eaa2 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 11 Feb 2022 20:02:53 +0000 Subject: [PATCH 15/32] Added a base class for all geo data In order to implement multiple geographic data types (pins, polygons, polylines, ...) there needs to be a standard implementation that the map can use. This base class is that --- src/markers.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/markers.ts b/src/markers.ts index cdc6922..11d0313 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -12,12 +12,54 @@ import * as consts from 'src/consts'; type MarkerId = string; -export class FileMarker { +export abstract class BaseGeoLayer { + /** + * The file descriptor + */ file: TFile; + /** + * The character position in the file the coordinate comes from + */ fileLocation?: number; + + /** + * The leaflet layer on the map + */ + geoLayer?: leaflet.Layer; + + /** + * Construct a new map pin object + * @param file + */ + protected constructor(file: TFile) { + this.file = file; + } + + /** + * Init the leaflet geographic layer from the data + * @param map + */ + abstract initGeoLayer(map: MapView): void; +} + + +/** + * A class to hold all the data for a map pin + */ +export class FileMarker extends BaseGeoLayer { + /** + * The coordinate for the geographic layer + */ location: leaflet.LatLng; + /** + * The image icon to display + */ icon?: leaflet.Icon; mapMarker?: leaflet.Marker; + /** + * The clickable object on the map + */ + geoLayer?: leaflet.Marker; id: MarkerId; snippet?: string; extraName?: string; @@ -29,7 +71,7 @@ export class FileMarker { * @param location */ constructor(file: TFile, location: leaflet.LatLng) { - this.file = file; + super(file) this.location = location; this.id = this.generateId(); } From c4cbf938bc1e9365a83ff5dfae2b849202bec1bf Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 11 Feb 2022 20:04:01 +0000 Subject: [PATCH 16/32] Increased max zoom and stopped tiles disappearing when zoomed in too far --- src/mapView.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 94e6a45..4f7795e 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -364,12 +364,15 @@ export class MapView extends ItemView { else revertMap = true; } - this.display.map.addLayer(new leaflet.TileLayer(mapSourceUrl, { - maxZoom: 20, - subdomains:['mt0','mt1','mt2','mt3'], - attribution: attribution, - className: revertMap ? "dark-mode" : "" - })); + this.display.map.addLayer( + new leaflet.TileLayer(mapSourceUrl, { + maxZoom: 25, + maxNativeZoom: 19, + subdomains:['mt0','mt1','mt2','mt3'], + attribution: attribution, + className: revertMap ? "dark-mode" : "" + }) + ); this.display.clusterGroup = new leaflet.MarkerClusterGroup({ maxClusterRadius: this.settings.maxClusterRadiusPixels ?? DEFAULT_SETTINGS.maxClusterRadiusPixels }); From 3df927a784a3589149fecf3e2b604924d04958b9 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Fri, 11 Feb 2022 20:09:58 +0000 Subject: [PATCH 17/32] Moved marker construction into the marker class Moved mapMarker to geoLayer Moved the marker init logic into the marker class so other geographic layer types can do the same. The BaseGeoLayer class defines the initGeoLayer which sets up the behaviour for that geographic type. It must populate the geoLayer field with a leaflet layer. --- src/mapView.ts | 49 ++++++--------------------------------- src/markers.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 4f7795e..5e3d0b6 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -44,7 +44,7 @@ type MapState = { * The map viewer class */ export class MapView extends ItemView { - private settings: PluginSettings; + readonly settings: PluginSettings; // The private state needs to be updated solely via updateMapToState private state: MapState; /** @@ -410,6 +410,8 @@ export class MapView extends ItemView { // this.display.clusterGroup.on('clustermouseout', cluster => { // // cluster.propagatedFrom.closePopup(); // }); + + // build the right click context menu this.display.map.on('contextmenu', async (event: leaflet.LeafletMouseEvent) => { let mapPopup = new Menu(this.app); mapPopup.setNoIcon(); @@ -532,55 +534,18 @@ export class MapView extends ItemView { this.display.markers.delete(marker.id); } else { // New marker - create it - marker.mapMarker = this.newLeafletMarker(marker); - markersToAdd.push(marker.mapMarker); + marker.initGeoLayer(this) + markersToAdd.push(marker.geoLayer); newMarkersMap.set(marker.id, marker); } } for (let [key, value] of this.display.markers) { - markersToRemove.push(value.mapMarker); + markersToRemove.push(value.geoLayer); } this.display.clusterGroup.addLayers(markersToAdd); this.display.clusterGroup.removeLayers(markersToRemove); - this.display.markers = newMarkersMap; - } - private newLeafletMarker(marker: FileMarker) : leaflet.Marker { - let newMarker = leaflet.marker(marker.location, { icon: marker.icon || new leaflet.Icon.Default() }); - newMarker.on('click', (event: leaflet.LeafletMouseEvent) => { - this.goToMarker(marker, event.originalEvent.ctrlKey, true); - }); - newMarker.on('mouseover', (event: leaflet.LeafletMouseEvent) => { - let content = `

${marker.file.name}

`; - if (marker.extraName) - content += `

${marker.extraName}

`; - if (marker.snippet) - content += `

${marker.snippet}

`; - newMarker.bindPopup(content, {closeButton: true, autoPan: false}).openPopup(); - }); - newMarker.on('mouseout', (event: leaflet.LeafletMouseEvent) => { - newMarker.closePopup(); - }); - newMarker.on('add', (event: leaflet.LeafletEvent) => { - newMarker.getElement().addEventListener('contextmenu', (ev: MouseEvent) => { - let mapPopup = new Menu(this.app); - mapPopup.setNoIcon(); - mapPopup.addItem((item: MenuItem) => { - item.setTitle('Open note'); - item.onClick(async ev => { this.goToMarker(marker, ev.ctrlKey, true); }); - }); - mapPopup.addItem((item: MenuItem) => { - item.setTitle('Open geolocation in default app'); - item.onClick(ev => { - open(`geo:${marker.location.lat},${marker.location.lng}`); - }); - }); - utils.populateOpenInItems(mapPopup, marker.location, this.settings); - mapPopup.showAtPosition(ev); - ev.stopPropagation(); - }) - }); - return newMarker; + this.display.markers = newMarkersMap; } /** diff --git a/src/markers.ts b/src/markers.ts index 11d0313..6037c94 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -1,4 +1,4 @@ -import { App, TFile, getAllTags } from 'obsidian'; +import {App, TFile, getAllTags, Menu, MenuItem} from 'obsidian'; import wildcard from 'wildcard'; import * as leaflet from 'leaflet'; import 'leaflet-extra-markers'; @@ -9,6 +9,8 @@ let localL = L; import { PluginSettings, MarkerIconRule } from 'src/settings'; import * as consts from 'src/consts'; +import * as utils from "src/utils"; +import type {MapView} from "src/mapView" type MarkerId = string; @@ -55,7 +57,6 @@ export class FileMarker extends BaseGeoLayer { * The image icon to display */ icon?: leaflet.Icon; - mapMarker?: leaflet.Marker; /** * The clickable object on the map */ @@ -99,6 +100,62 @@ export class FileMarker extends BaseGeoLayer { generateId() : MarkerId { return this.file.name + this.location.lat.toString() + this.location.lng.toString(); } + + /** + * Create a leaflet layer and add it to the map + */ + initGeoLayer(map: MapView): void { + this.icon = getIconForMarker(this, map.settings, map.app); + // create the leaflet marker instance + this.geoLayer = leaflet.marker(this.location, { icon: this.icon || new leaflet.Icon.Default() }); + // when clicked, open the marker in an editor + this.geoLayer.on('click', (event: leaflet.LeafletMouseEvent) => { + map.goToMarker(this, event.originalEvent.ctrlKey, true); + }); + // when hovered + this.geoLayer.on('mouseover', (event: leaflet.LeafletMouseEvent) => { + let content = `

${this.file.name}

`; + if (this.extraName) + content += `

${this.extraName}

`; + if (this.snippet) + content += `

${this.snippet}

`; + this.geoLayer.bindPopup(content, {closeButton: true, autoPan: false}).openPopup(); + }); + // when stop hovering + this.geoLayer.on( + 'mouseout', + (event: leaflet.LeafletMouseEvent) => { + this.geoLayer.closePopup(); + } + ); + // run when the layer is added to the map + this.geoLayer.on( + 'add', + (event: leaflet.LeafletEvent) => { + this.geoLayer.getElement().addEventListener( + 'contextmenu', + (ev: MouseEvent) => { + // on context menu creation + let mapPopup = new Menu(map.app); + mapPopup.setNoIcon(); + mapPopup.addItem((item: MenuItem) => { + item.setTitle('Open note'); + item.onClick(async ev => { map.goToMarker(this, ev.ctrlKey, true); }); + }); + mapPopup.addItem((item: MenuItem) => { + item.setTitle('Open geolocation in default app'); + item.onClick(ev => { + open(`geo:${this.location.lat},${this.location.lng}`); + }); + }); + utils.populateOpenInItems(mapPopup, this.location, map.settings); + mapPopup.showAtPosition(ev); + ev.stopPropagation(); + } + ) + } + ); + } } export type MarkersMap = Map; @@ -120,7 +177,6 @@ export async function buildAndAppendFileMarkers(mapToAppendTo: FileMarker[], fil if (location) { verifyLocation(location); let leafletMarker = new FileMarker(file, location); - leafletMarker.icon = getIconForMarker(leafletMarker, settings, app); mapToAppendTo.push(leafletMarker); } } @@ -245,7 +301,6 @@ async function getMarkersFromFileContent(file: TFile, settings: PluginSettings, marker.tags.push('#' + tag[1]); } marker.fileLocation = match.index; - marker.icon = getIconForMarker(marker, settings, app); marker.snippet = await makeTextSnippet(file, content, marker.fileLocation, settings); markers.push(marker); } From 37ea66ef45a3fe06495f4da54c204f5cee67a05d Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:07:40 +0000 Subject: [PATCH 18/32] Move the get and set state methods out of the constructor There may have been a reason for this but not that I can see --- src/mapView.ts | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 5e3d0b6..282b91c 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -122,22 +122,6 @@ export class MapView extends ItemView { tags: this.settings.defaultTags || consts.DEFAULT_TAGS, version: 0 }; - this.setState = async (state: MapState, result) => { - if (state) { - if (!state.version) { - // We give the given state priority by setting a high version - state.version = this.plugin.highestVersionSeen + 1; - } - if (!state.mapCenter || !state.mapZoom) { - state.mapCenter = this.defaultState.mapCenter; - state.mapZoom = this.defaultState.mapZoom; - } - await this.updateMapToState(state, false); - } - } - this.getState = (): MapState => { - return this.state; - } // Listen to file changes so we can rebuild the UI this.app.vault.on( @@ -158,6 +142,32 @@ export class MapView extends ItemView { }); } + /** + * Set the maps state. Used as part of the forwards/backwards arrows + * @param state The map state to set + * @param result + */ + async setState(state: MapState, result: ViewStateResult): Promise { + if (state) { + if (!state.version) { + // We give the given state priority by setting a high version + state.version = this.plugin.highestVersionSeen + 1; + } + if (!state.mapCenter || !state.mapZoom) { + state.mapCenter = this.defaultState.mapCenter; + state.mapZoom = this.defaultState.mapZoom; + } + await this.setMapState(state, false); + } + } + + /** + * Get the maps state. Used as part of the forwards/backwards arrows + */ + async getState(): Promise { + return this.state; + } + /** * The name of the view type */ From a448a102e9dee8b5c48195a38abac31f48459ccd Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:08:48 +0000 Subject: [PATCH 19/32] Removed input from isDarkMode Settings can be found in the class attributes --- src/mapView.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 282b91c..105cf28 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -180,12 +180,11 @@ export class MapView extends ItemView { /** * Is the map in dark mode - * @param settings */ - isDarkMode(settings: PluginSettings): boolean { - if (settings.chosenMapMode === 'dark') + isDarkMode(): boolean { + if (this.settings.chosenMapMode === 'dark') return true; - if (settings.chosenMapMode === 'light') + if (this.settings.chosenMapMode === 'light') return false; // Auto mode - check if the theme is dark if ((this.app.vault as any).getConfig('theme') === 'obsidian') @@ -347,7 +346,7 @@ export class MapView extends ItemView { } async createMap() { - const isDark = this.isDarkMode(this.settings); + const isDark = this.isDarkMode(); // LeafletJS compatability: disable tree-shaking for the full-screen module var dummy = leafletFullscreen; this.display.map = new leaflet.Map( From e526d453e8129a4e725e3eabc23a7ea0631436c2 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:11:55 +0000 Subject: [PATCH 20/32] Added missing ViewStateResult import --- src/mapView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mapView.ts b/src/mapView.ts index 105cf28..746f1d5 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -1,4 +1,4 @@ -import { TAbstractFile, Editor, ButtonComponent, getAllTags, ItemView, MenuItem, Menu, TFile, TextComponent, DropdownComponent, WorkspaceLeaf } from 'obsidian'; +import {TAbstractFile, Editor, ButtonComponent, getAllTags, ItemView, MenuItem, Menu, TFile, TextComponent, DropdownComponent, WorkspaceLeaf, ViewStateResult} from 'obsidian'; import * as leaflet from 'leaflet'; // Ugly hack for obsidian-leaflet compatability, see https://github.com/esm7/obsidian-map-view/issues/6 // @ts-ignore From 8b4c3dcb4dd5d63d15e2a49efe72fd242e953901 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:12:37 +0000 Subject: [PATCH 21/32] Reordered and documented the MapView attributes --- src/mapView.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 746f1d5..fa97f6d 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -44,8 +44,19 @@ type MapState = { * The map viewer class */ export class MapView extends ItemView { + /** + * The settings for the plugin + */ readonly settings: PluginSettings; - // The private state needs to be updated solely via updateMapToState + /** + * The default map state + * @private + */ + private defaultState: MapState; + /** + * The private map state. Must only be updated in setMapState + * @private + */ private state: MapState; /** * The map data @@ -82,11 +93,6 @@ export class MapView extends ItemView { * @private */ private plugin: MapViewPlugin; - /** - * The map state - * @private - */ - private defaultState: MapState; /** * The leaf (obsidian sub-window) that a note was last opened in. * This is cached so that it can be reused when opening notes in the future From fe1289da5a36f0d5d5ac513dea9e8a318cca705b Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:13:55 +0000 Subject: [PATCH 22/32] renamed updateMapToState to setMapState The new name more concisely describes what the method does. --- src/mapView.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index fa97f6d..7e3d461 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -241,7 +241,7 @@ export class MapView extends ItemView { this.display.tagsBox.setPlaceholder('Tags, e.g. "#one,#two"'); this.display.tagsBox.onChange(async (tagsBox: string) => { this.state.tags = tagsBox.split(',').filter(t => t.length > 0); - await this.updateMapToState(this.state, this.settings.autoZoom); + await this.setMapState(this.state, this.settings.autoZoom); }); let tagSuggestions = new DropdownComponent(filtersContent); tagSuggestions.setValue('Quick add tag'); @@ -301,7 +301,7 @@ export class MapView extends ItemView { tags: this.settings.defaultTags || consts.DEFAULT_TAGS, version: this.state.version + 1 }; - await this.updateMapToState(newState, false); + await this.setMapState(newState, false); }); let fitButton = new ButtonComponent(viewDivContent); fitButton @@ -348,7 +348,7 @@ export class MapView extends ItemView { this.display?.controlsDiv.remove(); this.display.controlsDiv = this.createControls(); this.createMap(); - this.updateMapToState(this.state, false, true); + this.setMapState(this.state, false, true); } async createMap() { @@ -476,15 +476,15 @@ export class MapView extends ItemView { } /** - * Updates the map to the given state and then sets the state accordingly, but only if the given state version - * is not lower than the current state version (so concurrent async updates always keep the latest one) - * @param state - * @param autoFit - * @param force + * Set the map state if the state version is not lower than the current state version + * (so concurrent async updates always keep the latest one) + * @param state The map state to set + * @param autoFit Should the view be fit to the markers + * @param force Force setting the state. Will ignore if the state is old */ - async updateMapToState(state: MapState, autoFit: boolean = false, force: boolean = false) { + async setMapState(state: MapState, autoFit: boolean = false, force: boolean = false) { if (this.settings.debug) - console.time('updateMapToState'); + console.time('setMapState'); const files = this.getFileListByQuery(state.tags); let newMarkers = await buildMarkers(files, this.settings, this.app); if (state.version < this.state.version && !force) { @@ -504,7 +504,7 @@ export class MapView extends ItemView { if (autoFit) this.autoFitMapToMarkers(); if (this.settings.debug) - console.timeEnd('updateMapToState'); + console.timeEnd('setMapState'); } /** From d5dc84f5bb271b86dfe642e6aa85f5209a8fcf78 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:14:35 +0000 Subject: [PATCH 23/32] Added more docstrings and comments to mapView --- src/mapView.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/mapView.ts b/src/mapView.ts index 7e3d461..1263cd7 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -221,10 +221,16 @@ export class MapView extends ItemView { return super.onOpen(); } + /** + * Set up the map controls + */ createControls() { + // html div container for the controls let controlsDiv = createDiv({ 'cls': 'graph-controls' }); + + // html div container for the filter entries let filtersDiv = controlsDiv.createDiv({'cls': 'graph-control-div'}); filtersDiv.innerHTML = ` @@ -237,12 +243,16 @@ export class MapView extends ItemView { this.plugin.saveSettings(); } let filtersContent = filtersDiv.createDiv({'cls': 'graph-control-content'}); + + // tag text entry this.display.tagsBox = new TextComponent(filtersContent); this.display.tagsBox.setPlaceholder('Tags, e.g. "#one,#two"'); this.display.tagsBox.onChange(async (tagsBox: string) => { this.state.tags = tagsBox.split(',').filter(t => t.length > 0); await this.setMapState(this.state, this.settings.autoZoom); }); + + // tag drop down let tagSuggestions = new DropdownComponent(filtersContent); tagSuggestions.setValue('Quick add tag'); tagSuggestions.addOption('', 'Quick add tag'); @@ -258,6 +268,7 @@ export class MapView extends ItemView { this.display.tagsBox.onChanged(); }); + // html div container for the view entries let viewDiv = controlsDiv.createDiv({'cls': 'graph-control-div'}); viewDiv.innerHTML = ` @@ -269,6 +280,8 @@ export class MapView extends ItemView { this.settings.mapControls.viewDisplayed = viewButton.checked; this.plugin.saveSettings(); } + + // map source drop down let viewDivContent = viewDiv.createDiv({'cls': 'graph-control-content'}); let mapSource = new DropdownComponent(viewDivContent); for (const [index, source] of this.settings.mapSources.entries()) { @@ -282,6 +295,8 @@ export class MapView extends ItemView { }); const chosenMapSource = this.settings.chosenMapSource ?? 0; mapSource.setValue(chosenMapSource.toString()); + + // the map theme drop down let sourceMode = new DropdownComponent(viewDivContent); sourceMode.addOptions({auto: 'Auto', light: 'Light', dark: 'Dark'}) .setValue(this.settings.chosenMapMode ?? 'auto') @@ -290,6 +305,8 @@ export class MapView extends ItemView { await this.plugin.saveSettings(); this.refreshMap(); }); + + // The reset view button let goDefault = new ButtonComponent(viewDivContent); goDefault .setButtonText('Reset') @@ -303,11 +320,15 @@ export class MapView extends ItemView { }; await this.setMapState(newState, false); }); + + // The fit view button let fitButton = new ButtonComponent(viewDivContent); fitButton .setButtonText('Fit') .setTooltip('Set the map view to fit all currently-displayed markers.') .onClick(() => this.autoFitMapToMarkers()); + + // The set view as default button let setDefault = new ButtonComponent(viewDivContent); setDefault .setButtonText('Set as Default') @@ -318,6 +339,7 @@ export class MapView extends ItemView { this.settings.defaultTags = this.state.tags; await this.plugin.saveSettings(); }); + this.contentEl.style.padding = '0px 0px'; this.contentEl.append(controlsDiv); return controlsDiv; @@ -351,6 +373,9 @@ export class MapView extends ItemView { this.setMapState(this.state, false, true); } + /** + * Create the leaflet map + */ async createMap() { const isDark = this.isDarkMode(); // LeafletJS compatability: disable tree-shaking for the full-screen module @@ -661,11 +686,13 @@ export class MapView extends ItemView { return; } let newMarkers: FileMarker[] = []; + // create an array of all file markers not in the removed file for (let [markerId, fileMarker] of this.display.markers) { if (fileMarker.file.path !== fileRemoved) newMarkers.push(fileMarker); } if (fileAddedOrChanged && fileAddedOrChanged instanceof TFile) + // add file markers from the added file await buildAndAppendFileMarkers(newMarkers, fileAddedOrChanged, this.settings, this.app) this.updateMapMarkers(newMarkers); } From 8bb779e8ff855e0f30022b897457473808a1563c Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 12 Feb 2022 21:33:29 +0000 Subject: [PATCH 24/32] Renamed updateMapMarkers to setMapMarkers This better describes what the function does. Update implies extending but this will remove points if they are not in the added markers. --- src/mapView.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 1263cd7..11e265f 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -521,7 +521,7 @@ export class MapView extends ItemView { // Saying it again: do not use 'await' below this line! this.state = state; this.plugin.highestVersionSeen = Math.max(this.plugin.highestVersionSeen, this.state.version); - this.updateMapMarkers(newMarkers); + this.setMapMarkers(newMarkers); this.state.tags = this.state.tags || []; this.display.tagsBox.setValue(this.state.tags.filter(tag => tag.length > 0).join(',')); if (this.state.mapCenter && this.state.mapZoom) @@ -558,14 +558,14 @@ export class MapView extends ItemView { } /** - * Remove markers that have changed or been removed and add the new ones - * @param newMarkers The new array of FileMarkers + * Set the markers on the map. + * @param markers The new array of FileMarkers */ - updateMapMarkers(newMarkers: FileMarker[]) { + setMapMarkers(markers: FileMarker[]) { let newMarkersMap: MarkersMap = new Map(); let markersToAdd: leaflet.Marker[] = []; let markersToRemove: leaflet.Marker[] = []; - for (let marker of newMarkers) { + for (let marker of markers) { const existingMarker = this.display.markers.has(marker.id) ? this.display.markers.get(marker.id) : null; if (existingMarker && existingMarker.isSame(marker)) { @@ -694,6 +694,6 @@ export class MapView extends ItemView { if (fileAddedOrChanged && fileAddedOrChanged instanceof TFile) // add file markers from the added file await buildAndAppendFileMarkers(newMarkers, fileAddedOrChanged, this.settings, this.app) - this.updateMapMarkers(newMarkers); + this.setMapMarkers(newMarkers); } } From 3538a5ff2c1fa77c0c94a5a4d9b6e8d15c04d705 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sun, 13 Feb 2022 20:01:26 +0000 Subject: [PATCH 25/32] Rewritten regular expressions to use named capture groups Renamed matchInlineLocation to matchInlineCoordinates to better describe what the function does. Rewritten regexes to use named capture groups to make them easier to maintain. --- src/main.ts | 6 +++--- src/markers.ts | 36 ++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main.ts b/src/main.ts index a2aa3c4..f401a2f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import { CoordinateParser } from 'src/coordinateParser'; import { MapView } from 'src/mapView'; import { PluginSettings, DEFAULT_SETTINGS, convertLegacyMarkerIcons, convertLegacyTilesUrl } from 'src/settings'; -import { getFrontMatterCoordinate, matchInlineLocation, verifyLocation } from 'src/markers'; +import { getFrontMatterCoordinate, matchInlineCoordinates, verifyLocation } from 'src/markers'; import { SettingsTab } from 'src/settingsTab'; import { NewNoteDialog } from 'src/newNoteDialog'; import * as utils from 'src/utils'; @@ -241,10 +241,10 @@ export default class MapViewPlugin extends Plugin { */ private getEditorLineCoordinate(editor: Editor, view: FileView): leaflet.LatLng { const line = editor.getLine(editor.getCursor().line); - const match = matchInlineLocation(line)[0]; + const match = matchInlineCoordinates(line)[0]; let selectedLocation = null; if (match) - selectedLocation = new leaflet.LatLng(parseFloat(match[2]), parseFloat(match[3])); + selectedLocation = new leaflet.LatLng(parseFloat(match.groups.lat), parseFloat(match.groups.long)); else { const fmLocation = getFrontMatterCoordinate(view.file, this.app); diff --git a/src/markers.ts b/src/markers.ts index 6037c94..09085a8 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -265,14 +265,14 @@ export function verifyLocation(location: leaflet.LatLng) { * Find all inline coordinates in a string * @param content The file contents to find the coordinates in */ -export function matchInlineLocation(content: string): RegExpMatchArray[] { +export function matchInlineCoordinates(content: string): RegExpMatchArray[] { // Old syntax of ` `location: ... ` `. This syntax doesn't support a name so we leave an empty capture group - const locationRegex1 = /\`()location:\s*\[?([0-9.\-]+)\s*,\s*([0-9.\-]+)\]?\`/g; + const legacyLocationRegex = /`location:\s*\[?(?[+-]?([0-9]*[.])?[0-9]+)\s*,\s*(?[+-]?([0-9]*[.])?[0-9]+)]?`/g // New syntax of `[name](geo:...)` and an optional tags as `tag:tagName` separated by whitespaces - const locationRegex2 = /\[(.*?)\]\(geo:([0-9.\-]+),([0-9.\-]+)\)[ \t]*((?:tag:[\w\/\-]+[\s\.]+)*)/g; - const matches1 = content.matchAll(locationRegex1); - const matches2 = content.matchAll(locationRegex2); - return Array.from(matches1).concat(Array.from(matches2)); + const geoURLlocationRegex = /\[(?.*?)]\(geo:(?[+-]?([0-9]*[.])?[0-9]+),(?[+-]?([0-9]*[.])?[0-9]+)\)[ \t]*(?(tag:[\w\/\-]+[\s.]+)*)/g; + const legacyMatches = content.matchAll(legacyLocationRegex); + const geoURLMatches = content.matchAll(geoURLlocationRegex); + return Array.from(legacyMatches).concat(Array.from(geoURLMatches)); } /** @@ -284,28 +284,28 @@ export function matchInlineLocation(content: string): RegExpMatchArray[] { async function getMarkersFromFileContent(file: TFile, settings: PluginSettings, app: App): Promise { let markers: FileMarker[] = []; const content = await app.vault.read(file); - const matches = matchInlineLocation(content); + const matches = matchInlineCoordinates(content); for (const match of matches) { try { - const location = new leaflet.LatLng(parseFloat(match[2]), parseFloat(match[3])); + const location = new leaflet.LatLng(parseFloat(match.groups.lat), parseFloat(match.groups.long)); verifyLocation(location); const marker = new FileMarker(file, location); - if (match[1] && match[1].length > 0) - marker.extraName = match[1]; - if (match[4]) { + if (match.groups.name && match.groups.name.length > 0) + marker.extraName = match.groups.name; + if (match.groups.tags) { // Parse the list of tags - const tagRegex = /tag:([\w\/\-]+)/g; - const tags = match[4].matchAll(tagRegex); + const tagRegex = /tag:(?[\w\/\-]+)/g; + const tags = match.groups.tags.matchAll(tagRegex); for (const tag of tags) - if (tag[1]) - marker.tags.push('#' + tag[1]); + if (tag.groups.tag) + marker.tags.push('#' + tag.groups.tag); } marker.fileLocation = match.index; marker.snippet = await makeTextSnippet(file, content, marker.fileLocation, settings); markers.push(marker); } catch (e) { - console.log(`Error converting location in file ${file.name}: could not parse ${match[1]} or ${match[2]}`, e); + console.log(`Error converting location in file ${file.name}: could not parse ${match[0]}`, e); } } return markers; @@ -323,7 +323,7 @@ async function makeTextSnippet(file: TFile, fileContent: string, fileLocation: n const prevLine = fileContent.lastIndexOf('\n', snippetStart - 1); const line = fileContent.substring(snippetStart, prevLine); // If the new line above contains another location, don't include it and stop - if (matchInlineLocation(line).length > 0) + if (matchInlineCoordinates(line).length > 0) break; snippetStart = prevLine; linesAbove -= 1; @@ -337,7 +337,7 @@ async function makeTextSnippet(file: TFile, fileContent: string, fileLocation: n const nextLine = fileContent.indexOf('\n', snippetEnd + 1); const line = fileContent.substring(snippetEnd, nextLine > -1 ? nextLine : fileContent.length); // If the new line below contains another location, don't include it and stop - if (matchInlineLocation(line).length > 0) + if (matchInlineCoordinates(line).length > 0) break; snippetEnd = nextLine; linesBelow -= 1; From 887be2c4a6f856c5221ce57af9009c7bf4dcf30c Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sun, 13 Feb 2022 20:36:28 +0000 Subject: [PATCH 26/32] Moved some FileMarker attributes into the base class --- src/markers.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/markers.ts b/src/markers.ts index 09085a8..7ac0df8 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -19,15 +19,30 @@ export abstract class BaseGeoLayer { * The file descriptor */ file: TFile; + /** + * The unique identifier for this geographic layer + */ + id: MarkerId; /** * The character position in the file the coordinate comes from */ fileLocation?: number; - /** * The leaflet layer on the map */ geoLayer?: leaflet.Layer; + /** + * Snippet of the file to show in the hover bubble + */ + snippet?: string; + /** + * Optional extra name. Used by geo urls with a prefixed name + */ + extraName?: string; + /** + * Any tags specified with the geographic layer + */ + tags: string[] = []; /** * Construct a new map pin object @@ -42,6 +57,18 @@ export abstract class BaseGeoLayer { * @param map */ abstract initGeoLayer(map: MapView): void; + + /** + * Generate a unique identifier for this layer + */ + abstract generateId(): MarkerId; + + /** + * Is this geographic layer identical to the other object. + * Used to compare to existing data to minimise creation. + * @param other The other object to compare to + */ + abstract isSame(other: BaseGeoLayer): boolean; } @@ -49,6 +76,8 @@ export abstract class BaseGeoLayer { * A class to hold all the data for a map pin */ export class FileMarker extends BaseGeoLayer { + geoLayer?: leaflet.Marker; + /** * The coordinate for the geographic layer */ @@ -60,11 +89,6 @@ export class FileMarker extends BaseGeoLayer { /** * The clickable object on the map */ - geoLayer?: leaflet.Marker; - id: MarkerId; - snippet?: string; - extraName?: string; - tags: string[] = []; /** * Construct a new map pin object @@ -81,7 +105,7 @@ export class FileMarker extends BaseGeoLayer { * Is the other data equal to this * @param other A FileMarker to compare to */ - isSame(other: FileMarker) { + isSame(other: FileMarker): boolean { return this.file.name === other.file.name && this.location.toString() === other.location.toString() && this.fileLocation === other.fileLocation && From 1b91b89f37c28afe4ddc09c7d038e3f0382f2371 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sun, 13 Feb 2022 21:14:30 +0000 Subject: [PATCH 27/32] Switched typing to the base geo layer class --- src/mapView.ts | 26 ++++++++++++++------------ src/markers.ts | 19 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 11e265f..5728acf 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -13,7 +13,7 @@ import 'leaflet.markercluster'; import * as consts from 'src/consts'; import { PluginSettings, MapLightDark, DEFAULT_SETTINGS } from 'src/settings'; -import { MarkersMap, FileMarker, buildMarkers, getIconFromOptions, buildAndAppendFileMarkers } from 'src/markers'; +import { MarkersMap, BaseGeoLayer, buildMarkers, getIconFromOptions, buildAndAppendGeoLayers } from 'src/markers'; import { LocationSuggest } from 'src/geosearch'; import MapViewPlugin from 'src/main'; import * as utils from 'src/utils'; @@ -35,7 +35,7 @@ type MapState = { */ tags: string[]; /** - * The version of the map to track if data is old + * The version of the state to work out which is newest */ version: number; } @@ -510,7 +510,9 @@ export class MapView extends ItemView { async setMapState(state: MapState, autoFit: boolean = false, force: boolean = false) { if (this.settings.debug) console.time('setMapState'); + // get a list of all files matching the tags const files = this.getFileListByQuery(state.tags); + // build the tags for all files matching the tag let newMarkers = await buildMarkers(files, this.settings, this.app); if (state.version < this.state.version && !force) { // If the state we were asked to update is old (e.g. because while we were building markers a newer instance @@ -561,10 +563,10 @@ export class MapView extends ItemView { * Set the markers on the map. * @param markers The new array of FileMarkers */ - setMapMarkers(markers: FileMarker[]) { + setMapMarkers(markers: BaseGeoLayer[]) { let newMarkersMap: MarkersMap = new Map(); - let markersToAdd: leaflet.Marker[] = []; - let markersToRemove: leaflet.Marker[] = []; + let markersToAdd: leaflet.Layer[] = []; + let markersToRemove: leaflet.Layer[] = []; for (let marker of markers) { const existingMarker = this.display.markers.has(marker.id) ? this.display.markers.get(marker.id) : null; @@ -648,7 +650,7 @@ export class MapView extends ItemView { * @param useCtrlKeyBehavior If true the file will be opened in a new instance * @param highlight If true will highlight the line */ - async goToMarker(marker: FileMarker, useCtrlKeyBehavior: boolean, highlight: boolean) { + async goToMarker(marker: BaseGeoLayer, useCtrlKeyBehavior: boolean, highlight: boolean) { return this.goToFile( marker.file, useCtrlKeyBehavior, @@ -685,15 +687,15 @@ export class MapView extends ItemView { // If the map has not been set up yet then do nothing return; } - let newMarkers: FileMarker[] = []; + let newGeoLayers: BaseGeoLayer[] = []; // create an array of all file markers not in the removed file - for (let [markerId, fileMarker] of this.display.markers) { - if (fileMarker.file.path !== fileRemoved) - newMarkers.push(fileMarker); + for (let [markerId, geoLayer] of this.display.markers) { + if (geoLayer.file.path !== fileRemoved) + newGeoLayers.push(geoLayer); } if (fileAddedOrChanged && fileAddedOrChanged instanceof TFile) // add file markers from the added file - await buildAndAppendFileMarkers(newMarkers, fileAddedOrChanged, this.settings, this.app) - this.setMapMarkers(newMarkers); + await buildAndAppendGeoLayers(newGeoLayers, fileAddedOrChanged, this.settings, this.app) + this.setMapMarkers(newGeoLayers); } } diff --git a/src/markers.ts b/src/markers.ts index 7ac0df8..4cc1a92 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -103,10 +103,11 @@ export class FileMarker extends BaseGeoLayer { /** * Is the other data equal to this - * @param other A FileMarker to compare to + * @param other A BaseGeoLayer to compare to */ - isSame(other: FileMarker): boolean { - return this.file.name === other.file.name && + isSame(other: BaseGeoLayer): boolean { + return other instanceof FileMarker && + this.file.name === other.file.name && this.location.toString() === other.location.toString() && this.fileLocation === other.fileLocation && this.extraName === other.extraName && @@ -182,7 +183,7 @@ export class FileMarker extends BaseGeoLayer { } } -export type MarkersMap = Map; +export type MarkersMap = Map; /** * Create file markers for every coordinate in the front matter and file body @@ -192,7 +193,7 @@ export type MarkersMap = Map; * @param app The obsidian app instance * @param skipMetadata If true will not find markers in the front matter */ -export async function buildAndAppendFileMarkers(mapToAppendTo: FileMarker[], file: TFile, settings: PluginSettings, app: App, skipMetadata?: boolean) { +export async function buildAndAppendGeoLayers(mapToAppendTo: BaseGeoLayer[], file: TFile, settings: PluginSettings, app: App, skipMetadata?: boolean) { const fileCache = app.metadataCache.getFileCache(file); const frontMatter = fileCache?.frontmatter; if (frontMatter) { @@ -217,12 +218,12 @@ export async function buildAndAppendFileMarkers(mapToAppendTo: FileMarker[], fil * @param settings * @param app */ -export async function buildMarkers(files: TFile[], settings: PluginSettings, app: App): Promise { +export async function buildMarkers(files: TFile[], settings: PluginSettings, app: App): Promise { if (settings.debug) console.time('buildMarkers'); - let markers: FileMarker[] = []; + let markers: BaseGeoLayer[] = []; for (const file of files) { - await buildAndAppendFileMarkers(markers, file, settings, app); + await buildAndAppendGeoLayers(markers, file, settings, app); } if (settings.debug) console.timeEnd('buildMarkers'); @@ -240,7 +241,7 @@ function checkTagPatternMatch(tagPattern: string, tags: string[]) { * @param settings The plugin settings * @param app The obsidian app instance */ -function getIconForMarker(marker: FileMarker, settings: PluginSettings, app: App) : leaflet.Icon { +function getIconForMarker(marker: BaseGeoLayer, settings: PluginSettings, app: App) : leaflet.Icon { const fileCache = app.metadataCache.getFileCache(marker.file); // Combine the file tags with the marker-specific tags const tags = getAllTags(fileCache).concat(marker.tags); From b149bd8204d0ed2d9762604bad0da61f851dcf26 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 14 Feb 2022 13:02:24 +0000 Subject: [PATCH 28/32] Fixed a compile error when accessing the location attribute Added getBounds method so that it can work with points and shapes --- src/mapView.ts | 5 ++++- src/markers.ts | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mapView.ts b/src/mapView.ts index 5728acf..630a711 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -595,7 +595,10 @@ export class MapView extends ItemView { */ async autoFitMapToMarkers() { if (this.display.markers.size > 0) { - const locations: leaflet.LatLng[] = Array.from(this.display.markers.values()).map(fileMarker => fileMarker.location); + let locations: leaflet.LatLng[] = [] + for (let layer of this.display.markers.values()) { + locations.push(...layer.getBounds()); + } console.log(`Auto fit by state:`, this.state); this.display.map.fitBounds(leaflet.latLngBounds(locations)); } diff --git a/src/markers.ts b/src/markers.ts index 4cc1a92..be1bc5f 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -69,6 +69,11 @@ export abstract class BaseGeoLayer { * @param other The other object to compare to */ abstract isSame(other: BaseGeoLayer): boolean; + + /** + * Get the bounds of the data + */ + abstract getBounds(): leaflet.LatLng[]; } @@ -181,6 +186,10 @@ export class FileMarker extends BaseGeoLayer { } ); } + + getBounds(): leaflet.LatLng[] { + return [this.location]; + } } export type MarkersMap = Map; From 4feae767b4fdb9e777ad38d3b32ac314e14680df Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 14 Feb 2022 16:57:50 +0000 Subject: [PATCH 29/32] Added geoJSON viewing support --- src/mapView.ts | 8 +-- src/markers.ts | 132 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/mapView.ts b/src/mapView.ts index 630a711..8e93e8d 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -576,9 +576,11 @@ export class MapView extends ItemView { this.display.markers.delete(marker.id); } else { // New marker - create it - marker.initGeoLayer(this) - markersToAdd.push(marker.geoLayer); - newMarkersMap.set(marker.id, marker); + try { + marker.initGeoLayer(this) + markersToAdd.push(marker.geoLayer); + newMarkersMap.set(marker.id, marker); + } catch (e) {} } } for (let [key, value] of this.display.markers) { diff --git a/src/markers.ts b/src/markers.ts index be1bc5f..b27aba6 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -11,9 +11,29 @@ import { PluginSettings, MarkerIconRule } from 'src/settings'; import * as consts from 'src/consts'; import * as utils from "src/utils"; import type {MapView} from "src/mapView" +import {GeoJsonObject} from 'geojson'; +import {Arr} from "tern"; type MarkerId = string; + +if (!(leaflet.Polygon.prototype as any).getLatLng) { + // extend the Polygon behaviour to support clustering + leaflet.Polygon.addInitHook(function() { + console.log(this.getBounds().getCenter()) + this._latlng = this.getBounds().getCenter(); + }); + + leaflet.Polygon.include({ + getLatLng: function() { + return this._latlng; + }, + setLatLng: function() {} // Dummy method. + }); +} + + + export abstract class BaseGeoLayer { /** * The file descriptor @@ -77,6 +97,48 @@ export abstract class BaseGeoLayer { } +export class GeoJSON extends BaseGeoLayer { + /** + * The raw geoJSON data + */ + geoJSON: GeoJsonObject; + + /** + * The GeoJSON leaflet object + */ + geoLayer?: leaflet.GeoJSON + + constructor(file: TFile, geoJSON: GeoJsonObject) { + super(file); + this.geoJSON = geoJSON + this.id = this.generateId(); + } + + initGeoLayer(map: MapView) { + try { + this.geoLayer = new leaflet.GeoJSON(this.geoJSON); + } catch (e) { + console.log("geoJSON is not valid geoJSON", this.geoJSON) + throw e; + } + } + + generateId(): MarkerId { + return this.file.name + JSON.stringify(this.geoJSON); + } + + isSame(other: BaseGeoLayer): boolean { + return other instanceof GeoJSON && + other.geoJSON == this.geoJSON; + } + + getBounds(): leaflet.LatLng[] { + let bounds = this.geoLayer.getBounds() + return [bounds.getNorthEast(), bounds.getSouthWest()]; + } +} + + /** * A class to hold all the data for a map pin */ @@ -213,10 +275,16 @@ export async function buildAndAppendGeoLayers(mapToAppendTo: BaseGeoLayer[], fil let leafletMarker = new FileMarker(file, location); mapToAppendTo.push(leafletMarker); } + let geoJSON = getFrontMatterGeoJSON(file, app); + if (geoJSON) { + mapToAppendTo.push(new GeoJSON(file, geoJSON)); + } } if ('locations' in frontMatter) { - const markersFromFile = await getMarkersFromFileContent(file, settings, app); - mapToAppendTo.push(...markersFromFile); + const inlineCoordiantes = await getInlineFileMarkers(file, settings, app); + mapToAppendTo.push(...inlineCoordiantes); + const inlineGeoJSON = await getInlineGeoJSON(file, settings, app); + mapToAppendTo.push(...inlineGeoJSON); } } } @@ -309,13 +377,24 @@ export function matchInlineCoordinates(content: string): RegExpMatchArray[] { return Array.from(legacyMatches).concat(Array.from(geoURLMatches)); } +/** + * Find all inline geoJSON in a string + * @param content The file contents to find the coordinates in + */ +export function matchInlineGeoJSON(content: string): RegExpMatchArray[] { + // New syntax of `[name](geo:...)` and an optional tags as `tag:tagName` separated by whitespaces + const geoJSONlocationRegex = /```geoJSON\n(?.*?)```/gs; + const geoJSONMatches = content.matchAll(geoJSONlocationRegex); + return Array.from(geoJSONMatches); +} + /** * Get markers from within the file body * @param file The file descriptor to load * @param settings The plugin settings * @param app The obsidian app instance */ -async function getMarkersFromFileContent(file: TFile, settings: PluginSettings, app: App): Promise { +async function getInlineFileMarkers(file: TFile, settings: PluginSettings, app: App): Promise { let markers: FileMarker[] = []; const content = await app.vault.read(file); const matches = matchInlineCoordinates(content); @@ -345,6 +424,38 @@ async function getMarkersFromFileContent(file: TFile, settings: PluginSettings, return markers; } +/** + * Get geoJSON from within the file body + * @param file The file descriptor to load + * @param settings The plugin settings + * @param app The obsidian app instance + */ +async function getInlineGeoJSON(file: TFile, settings: PluginSettings, app: App): Promise { + let markers: GeoJSON[] = []; + const content = await app.vault.read(file); + const matches = matchInlineGeoJSON(content); + for (const match of matches) { + try { + const geoJSONData = JSON.parse(match.groups.geoJSON); + const geoJSON = new GeoJSON(file, geoJSONData); + if (geoJSONData instanceof Object && geoJSONData.tags && geoJSONData.tags instanceof Array) { + for (const tag of geoJSONData.tags) { + if (tag instanceof String) { + geoJSON.tags.push('#' + tag); + } + } + } + geoJSON.fileLocation = match.index; + geoJSON.snippet = await makeTextSnippet(file, content, geoJSON.fileLocation, settings); + markers.push(geoJSON); + } + catch (e) { + console.log(`Error converting inline geoJSON from ${file.name}: could not parse ${match.groups.geoJSON}`, e); + } + } + return markers; +} + async function makeTextSnippet(file: TFile, fileContent: string, fileLocation: number, settings: PluginSettings) { let snippet = ''; if (settings.snippetLines && settings.snippetLines > 0) { @@ -411,3 +522,18 @@ export function getFrontMatterCoordinate(file: TFile, app: App) : leaflet.LatLng } return null; } + +/** + * Get the geoJSON stored in the front matter of a file + * @param file The file to load the front matter from + * @param app The app to load the file from + */ +export function getFrontMatterGeoJSON(file: TFile, app: App) : GeoJsonObject { + const fileCache = app.metadataCache.getFileCache(file); + const frontMatter = fileCache?.frontmatter; + if (frontMatter && frontMatter?.geoJSON) { + console.log(frontMatter); + return frontMatter.geoJSON; + } + return null; +} \ No newline at end of file From 9c86382d627630736852ca96f517b1ab4846385b Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 14 Feb 2022 19:28:16 +0000 Subject: [PATCH 30/32] Removed incorrectly imported variable pyCharm added this when I tried to type Array --- src/markers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/markers.ts b/src/markers.ts index b27aba6..10142df 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -12,7 +12,6 @@ import * as consts from 'src/consts'; import * as utils from "src/utils"; import type {MapView} from "src/mapView" import {GeoJsonObject} from 'geojson'; -import {Arr} from "tern"; type MarkerId = string; From 343c6e613f8bbade95fe900b0ca2031ed9ac9d71 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Mon, 14 Feb 2022 19:55:13 +0000 Subject: [PATCH 31/32] Added in the UI overlay for geoJSON object This could probably be improved with better support for the other layer types. --- src/markers.ts | 57 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/markers.ts b/src/markers.ts index 10142df..5529af4 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -19,7 +19,6 @@ type MarkerId = string; if (!(leaflet.Polygon.prototype as any).getLatLng) { // extend the Polygon behaviour to support clustering leaflet.Polygon.addInitHook(function() { - console.log(this.getBounds().getCenter()) this._latlng = this.getBounds().getCenter(); }); @@ -96,12 +95,16 @@ export abstract class BaseGeoLayer { } -export class GeoJSON extends BaseGeoLayer { +export class GeoJSONLayer extends BaseGeoLayer { /** * The raw geoJSON data */ geoJSON: GeoJsonObject; + /** + * The image icon to display for pins + */ + icon?: leaflet.Icon; /** * The GeoJSON leaflet object */ @@ -114,8 +117,42 @@ export class GeoJSON extends BaseGeoLayer { } initGeoLayer(map: MapView) { + const geoJSONLayer = this; try { - this.geoLayer = new leaflet.GeoJSON(this.geoJSON); + this.geoLayer = new leaflet.GeoJSON( + this.geoJSON, + { + onEachFeature(_, layer: leaflet.Layer) { + if (layer instanceof leaflet.Marker){ + if (!geoJSONLayer.icon){ + geoJSONLayer.icon = getIconForMarker(geoJSONLayer, map.settings, map.app); + } + layer.setIcon(geoJSONLayer.icon) + } + + // when clicked, open the marker in an editor + layer.on('click', (event: leaflet.LeafletMouseEvent) => { + map.goToMarker(geoJSONLayer, event.originalEvent.ctrlKey, true); + }); + // when hovered + layer.on('mouseover', (event: leaflet.LeafletMouseEvent) => { + let content = `

${geoJSONLayer.file.name}

`; + if (geoJSONLayer.extraName) + content += `

${geoJSONLayer.extraName}

`; + if (geoJSONLayer.snippet) + content += `

${geoJSONLayer.snippet}

`; + layer.bindPopup(content, {closeButton: true, autoPan: false}).openPopup(); + }); + // when stop hovering + layer.on( + 'mouseout', + (event: leaflet.LeafletMouseEvent) => { + layer.closePopup(); + } + ); + } + } + ); } catch (e) { console.log("geoJSON is not valid geoJSON", this.geoJSON) throw e; @@ -127,7 +164,7 @@ export class GeoJSON extends BaseGeoLayer { } isSame(other: BaseGeoLayer): boolean { - return other instanceof GeoJSON && + return other instanceof GeoJSONLayer && other.geoJSON == this.geoJSON; } @@ -152,9 +189,6 @@ export class FileMarker extends BaseGeoLayer { * The image icon to display */ icon?: leaflet.Icon; - /** - * The clickable object on the map - */ /** * Construct a new map pin object @@ -276,7 +310,7 @@ export async function buildAndAppendGeoLayers(mapToAppendTo: BaseGeoLayer[], fil } let geoJSON = getFrontMatterGeoJSON(file, app); if (geoJSON) { - mapToAppendTo.push(new GeoJSON(file, geoJSON)); + mapToAppendTo.push(new GeoJSONLayer(file, geoJSON)); } } if ('locations' in frontMatter) { @@ -429,14 +463,14 @@ async function getInlineFileMarkers(file: TFile, settings: PluginSettings, app: * @param settings The plugin settings * @param app The obsidian app instance */ -async function getInlineGeoJSON(file: TFile, settings: PluginSettings, app: App): Promise { - let markers: GeoJSON[] = []; +async function getInlineGeoJSON(file: TFile, settings: PluginSettings, app: App): Promise { + let markers: GeoJSONLayer[] = []; const content = await app.vault.read(file); const matches = matchInlineGeoJSON(content); for (const match of matches) { try { const geoJSONData = JSON.parse(match.groups.geoJSON); - const geoJSON = new GeoJSON(file, geoJSONData); + const geoJSON = new GeoJSONLayer(file, geoJSONData); if (geoJSONData instanceof Object && geoJSONData.tags && geoJSONData.tags instanceof Array) { for (const tag of geoJSONData.tags) { if (tag instanceof String) { @@ -531,7 +565,6 @@ export function getFrontMatterGeoJSON(file: TFile, app: App) : GeoJsonObject { const fileCache = app.metadataCache.getFileCache(file); const frontMatter = fileCache?.frontmatter; if (frontMatter && frontMatter?.geoJSON) { - console.log(frontMatter); return frontMatter.geoJSON; } return null; From 11477777418f2dd01b22893af4568e4c4b752b2a Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Wed, 16 Feb 2022 18:52:03 +0000 Subject: [PATCH 32/32] Compacted single line docstrings --- src/coordinateParser.ts | 4 +- src/main.ts | 12 ++--- src/mapView.ts | 100 ++++++++++------------------------------ src/markers.ts | 64 +++++++------------------ 4 files changed, 45 insertions(+), 135 deletions(-) diff --git a/src/coordinateParser.ts b/src/coordinateParser.ts index d2bbf16..882315a 100644 --- a/src/coordinateParser.ts +++ b/src/coordinateParser.ts @@ -12,9 +12,7 @@ interface FindResult { ruleName: string } -/** - * A class to convert a string (usually a URL) into coordinate format - */ +/** A class to convert a string (usually a URL) into coordinate format */ export class CoordinateParser { private settings: PluginSettings; diff --git a/src/main.ts b/src/main.ts index f401a2f..dbb0019 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,9 +11,7 @@ import { SettingsTab } from 'src/settingsTab'; import { NewNoteDialog } from 'src/newNoteDialog'; import * as utils from 'src/utils'; -/** - * A plugin to implement map support - */ +/** A plugin to implement map support */ export default class MapViewPlugin extends Plugin { settings: PluginSettings; public highestVersionSeen: number = 0; @@ -261,16 +259,12 @@ export default class MapViewPlugin extends Plugin { onunload() { } - /** - * Load the settings from disk - */ + /** Load the settings from disk */ async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } - /** - * Save the settings to disk - */ + /** Save the settings to disk */ async saveSettings() { await this.saveData(this.settings); } diff --git a/src/mapView.ts b/src/mapView.ts index 8e93e8d..6c0d06f 100644 --- a/src/mapView.ts +++ b/src/mapView.ts @@ -18,35 +18,21 @@ import { LocationSuggest } from 'src/geosearch'; import MapViewPlugin from 'src/main'; import * as utils from 'src/utils'; -/** - * The state of the map instance - */ +/** The state of the map instance */ type MapState = { - /** - * The zoom level of the map - */ + /** The zoom level of the map */ mapZoom: number; - /** - * The viewed center of the map - */ + /** The viewed center of the map */ mapCenter: leaflet.LatLng; - /** - * The tags that the user specified - */ + /** The tags that the user specified */ tags: string[]; - /** - * The version of the state to work out which is newest - */ + /** The version of the state to work out which is newest */ version: number; } -/** - * The map viewer class - */ +/** The map viewer class */ export class MapView extends ItemView { - /** - * The settings for the plugin - */ + /** The settings for the plugin */ readonly settings: PluginSettings; /** * The default map state @@ -63,29 +49,17 @@ export class MapView extends ItemView { * @private */ private display = new class { - /** - * The leaflet map instance for this map viewer - */ + /** The leaflet map instance for this map viewer */ map: leaflet.Map; - /** - * The cluster management class - */ + /** The cluster management class */ clusterGroup: leaflet.MarkerClusterGroup; - /** - * The markers currently on the map - */ + /** The markers currently on the map */ markers: MarkersMap = new Map(); - /** - * The HTML element containing the map controls - */ + /** The HTML element containing the map controls */ controlsDiv: HTMLDivElement; - /** - * The HTML element holding the map - */ + /** The HTML element holding the map */ mapDiv: HTMLDivElement; - /** - * The HTML text entry the user can type tags in - */ + /** The HTML text entry the user can type tags in */ tagsBox: TextComponent; }; /** @@ -105,9 +79,7 @@ export class MapView extends ItemView { */ private isOpen: boolean = false; - /** - * TODO: unused what does this do? - */ + /** TODO: unused what does this do? */ public onAfterOpen: (map: leaflet.Map, markers: MarkersMap) => any = null; /** @@ -167,26 +139,18 @@ export class MapView extends ItemView { } } - /** - * Get the maps state. Used as part of the forwards/backwards arrows - */ + /** Get the maps state. Used as part of the forwards/backwards arrows */ async getState(): Promise { return this.state; } - /** - * The name of the view type - */ + /** The name of the view type */ getViewType() { return consts.MAP_VIEW_NAME; } - /** - * The display name for the view - */ + /** The display name for the view */ getDisplayText() { return 'Interactive Map View'; } - /** - * Is the map in dark mode - */ + /** Is the map in dark mode */ isDarkMode(): boolean { if (this.settings.chosenMapMode === 'dark') return true; @@ -200,9 +164,7 @@ export class MapView extends ItemView { public updateMapSources = () => {}; - /** - * Run when the view is opened - */ + /** Run when the view is opened */ async onOpen() { this.isOpen = true; this.state = this.defaultState; @@ -221,9 +183,7 @@ export class MapView extends ItemView { return super.onOpen(); } - /** - * Set up the map controls - */ + /** Set up the map controls */ createControls() { // html div container for the controls let controlsDiv = createDiv({ @@ -345,17 +305,13 @@ export class MapView extends ItemView { return controlsDiv; } - /** - * On view close - */ + /** On view close */ onClose() { this.isOpen = false; return super.onClose(); } - /** - * On window resize - */ + /** On window resize */ onResize() { this.display.map.invalidateSize(); } @@ -373,9 +329,7 @@ export class MapView extends ItemView { this.setMapState(this.state, false, true); } - /** - * Create the leaflet map - */ + /** Create the leaflet map */ async createMap() { const isDark = this.isDarkMode(); // LeafletJS compatability: disable tree-shaking for the full-screen module @@ -592,9 +546,7 @@ export class MapView extends ItemView { this.display.markers = newMarkersMap; } - /** - * Zoom the map to fit all markers on the screen - */ + /** Zoom the map to fit all markers on the screen */ async autoFitMapToMarkers() { if (this.display.markers.size > 0) { let locations: leaflet.LatLng[] = [] @@ -663,9 +615,7 @@ export class MapView extends ItemView { ); } - /** - * Get a list of all tags in the archive - */ + /** Get a list of all tags in the archive */ getAllTagNames() : string[] { let tags: string[] = []; const allFiles = this.app.vault.getFiles(); diff --git a/src/markers.ts b/src/markers.ts index 5529af4..b1ca62e 100644 --- a/src/markers.ts +++ b/src/markers.ts @@ -33,33 +33,19 @@ if (!(leaflet.Polygon.prototype as any).getLatLng) { export abstract class BaseGeoLayer { - /** - * The file descriptor - */ + /** The file descriptor */ file: TFile; - /** - * The unique identifier for this geographic layer - */ + /** The unique identifier for this geographic layer */ id: MarkerId; - /** - * The character position in the file the coordinate comes from - */ + /** The character position in the file the coordinate comes from */ fileLocation?: number; - /** - * The leaflet layer on the map - */ + /** The leaflet layer on the map */ geoLayer?: leaflet.Layer; - /** - * Snippet of the file to show in the hover bubble - */ + /** Snippet of the file to show in the hover bubble */ snippet?: string; - /** - * Optional extra name. Used by geo urls with a prefixed name - */ + /** Optional extra name. Used by geo urls with a prefixed name */ extraName?: string; - /** - * Any tags specified with the geographic layer - */ + /** Any tags specified with the geographic layer */ tags: string[] = []; /** @@ -76,9 +62,7 @@ export abstract class BaseGeoLayer { */ abstract initGeoLayer(map: MapView): void; - /** - * Generate a unique identifier for this layer - */ + /** Generate a unique identifier for this layer */ abstract generateId(): MarkerId; /** @@ -88,26 +72,18 @@ export abstract class BaseGeoLayer { */ abstract isSame(other: BaseGeoLayer): boolean; - /** - * Get the bounds of the data - */ + /** Get the bounds of the data */ abstract getBounds(): leaflet.LatLng[]; } export class GeoJSONLayer extends BaseGeoLayer { - /** - * The raw geoJSON data - */ + /** The raw geoJSON data */ geoJSON: GeoJsonObject; - /** - * The image icon to display for pins - */ + /** The image icon to display for pins */ icon?: leaflet.Icon; - /** - * The GeoJSON leaflet object - */ + /** The GeoJSON leaflet object */ geoLayer?: leaflet.GeoJSON constructor(file: TFile, geoJSON: GeoJsonObject) { @@ -175,19 +151,13 @@ export class GeoJSONLayer extends BaseGeoLayer { } -/** - * A class to hold all the data for a map pin - */ +/** A class to hold all the data for a map pin */ export class FileMarker extends BaseGeoLayer { geoLayer?: leaflet.Marker; - /** - * The coordinate for the geographic layer - */ + /** The coordinate for the geographic layer */ location: leaflet.LatLng; - /** - * The image icon to display - */ + /** The image icon to display */ icon?: leaflet.Icon; /** @@ -226,9 +196,7 @@ export class FileMarker extends BaseGeoLayer { return this.file.name + this.location.lat.toString() + this.location.lng.toString(); } - /** - * Create a leaflet layer and add it to the map - */ + /** Create a leaflet layer and add it to the map */ initGeoLayer(map: MapView): void { this.icon = getIconForMarker(this, map.settings, map.app); // create the leaflet marker instance