Cleanups and README writeups

This commit is contained in:
Erez Shermer 2025-07-23 21:54:19 +03:00
parent 255afb5128
commit 2c74a16443
20 changed files with 208 additions and 150 deletions

View file

@ -410,59 +410,60 @@ And/or you can have notes to plan a trip and link to places from it, then use `l
In all cases you can [save presets](#Presets) that include the filter or sub-filters of it.
## Marker Icons
## Marker & Path Display Rules
TODO - this requires a major update and also updating the links that lead here, including from the code!
TODO - update links that go here, from the document and code!
Map View allows you to customize notes' map marker icons based on a powerful rules system.
These rules can be edited using the plugin's settings pane or edited as JSON for some even more fine-grained control.
Map View includes a powerful mechanism that allows you to customize map markers and path display properties based on a powerful rules system. These are called Display Rules.
Display rules are made of a **query**, which is the same as discussed [here](#queries), and various properties to apply to a marker or a path that match the query.
When Map View tries to decide how to display a marker or a path, it starts from the Default display rule, which has a certain set of properties (e.g. a blue marker with a circle). It then tests the rest of the display rules by order, and for each rule with a matching query, overwrites whatever properties that rule sets.
For example, the default rule for markers may be a blue marker color with an `fa-circle` icon. If the marker also matches a rule below the default one (e.g. a rule like `tag:#food`), and that rule sets just the icon to `fa-utensils`, the resulting marker will be blue (because the 2nd rule did not override the color) with an `fa-utensils` icon.
(Note to Map View users prior to version 6.0.0: this is the same way Marker Icons worked before, except that rules use _queries_ rather than tags, allowing more flexibility.)
To edit display rules, open the plugin settings, and click the button under the "Marker & Path Display Rules" section.
You will see the list of the currently-active rules. You can add new rules, change the order of rules (except the default which must be first), and edit existing rules.
When editing a rule, you will be able to set various properties that the rule can apply. All these properties are optional, and the rule will overwrite the default (or prior matching rules) for every marker or path that will match its query.
### Marker Icon Properties
Icons are based on either [emojis](https://emojipedia.org) or [Font Awesome](https://fontawesome.com/), so to add a marker icon you'll need to copy the emoji or find the name in the Font Awesome catalog.
Additionally, there are various marker properties (shape, color and more) that are based on [Leaflet.ExtraMarkers](https://github.com/coryasilva/Leaflet.ExtraMarkers#properties).
An additional shape of `simple-circle`, that draws a circle at the given color without a pin shape, is supported on top of these.
To change the map marker icons for your notes, go to the Map View settings and scroll to Marker Icon Rules.
For example, if you want markers with the `#travel` tag to have a bus icon, add a display rule with the query `tag:#travel`. Then click Edit, search the Font Awesome catalog for the appropriate icon (in this case `fa-bus`), and enter that in the 'icon' box.
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.
![](img/marker-rules.png)
A single marker is defined in the following JSON structure:
`{"prefix": "fas", "icon": "fa-bus", "shape": "circle", "color": "red"}`
To add a marker with a bus icon, click New Icon Rule, search Font Awesome (in the link above) for 'bus', choose [this icon](https://fontawesome.com/v5.15/icons/bus?style=solid), then see that its name is `fa-bus`.
Once you enter `fa-bus` in the icon name, you should immediately see your icon in the preview.
To make this icon apply for notes with the `#travel` tag, type `#travel` in the Tag Name box.
Alternatively, just paste an emoji of a bus (e.g. from [Emojipedia](https://emojipedia.org)) into the icon name box.
### Tag Rules
### Marker Badge
To apply an icon to a note with geolocation data, Map View scans the complete list of rules by their order, always starting from `default`.
A rule matches if the tag that it lists is included in the note, and then the rule's fields will overwrite the corresponding fields of the previous matching rules, until all rules were scanned.
This allows you to set rules that change just some properties of the icons, e.g. some rules change the shape according to some tags, some change the color etc.
You can add more information to markers using **badges**, which are little circles that are added in the corners of markers based on criteria that you choose.
Up to 4 badges are supported per marker.
Here's the example I provide as a probably-not-useful default in the plugin:
Display rules can mix icon properties with badges in any way you see fit.
```
{ruleName: "default", preset: true, iconDetails: {"prefix": "fas", "icon": "fa-circle", "markerColor": "blue"}},
{ruleName: "#trip", preset: false, iconDetails: {"prefix": "fas", "icon": "fa-hiking", "markerColor": "green"}},
{ruleName: "#trip-water", preset: false, iconDetails: {"prefix": "fas", "markerColor": "blue"}},
{ruleName: "#dogs", preset: false, iconDetails: {"prefix": "fas", "icon": "fa-paw"}},
```
To add a badge to a display rule, in the display rule edit dialog, paste an emoji or up to 2 characters into the Symbol box. Markers that match the rule will have that badge, and possibly other badges from other matching display rules, starting from the top-left corner clockwise.
This means that all notes will have a blue `fa-circle` icon by default.
However, a note with the `#trip` tag will have a green `fa-hiking` icon.
Then, a note that has both the `#trip` and `#trip-water` tags will have a `fa-hiking` marker (when the `#trip` rule is applied), but a **blue** marker, because the `#trip-water` overwrites the `markerColor` that the previous `#trip` rule has set.
Badges can have a symbol, a text color, a background color, and a border in the syntax of a CSS [border](https://developer.mozilla.org/en-US/docs/Web/CSS/border) property, e.g. `1px solid black`.
Tag rules also support wildcards, e.g. a rule in the form of `"#food*": {...}` will match notes with the tag `#food`, `#food/pizza`, `#food/vegan`, `#food-to-try` etc.
### Path Properties
The settings also allow advanced users to manually edit the configuration tree, and there you can use more properties based on the [Leaflet.ExtraMarkers](https://github.com/coryasilva/Leaflet.ExtraMarkers#properties) properties. Manual edits update the GUI in real-time.
Paths work exactly like markers, in the way that their style starts from the default rule, and properties of matching rules overwrite each other.
Similarly to markers, you can set a rule like `tag:#hike` to a path and set to rule to have `red` for color.
## Badges
One difference is that some types of paths (e.g. GPX attachments to the vault) do not have a way to attach a tag to them; if you want to style some paths differently than others, you can use other query types, like name or `linkedfrom`. For example, you can have a central note named "My Runs" that will link to all the GPX files you want styled differently, and use a `linkedfrom` query in a display rule that will set these to red. See more about how to write such queries [here](#queries).
TODO write
Also, paths do not supported badges.
### Advanced: Editing Rules as JSON
The Edit Rule dialog allows you to directly edit a display rule as JSON, allowing a few more options and control than the GUI provides.
- More options for marker icons can be found in [Leaflet.ExtraMarkers](https://github.com/coryasilva/Leaflet.ExtraMarkers#properties) properties. Note that some of these properties are known to not work with Map View.
- Path options reference can be found [here](https://leafletjs.com/reference.html#path).
- Marker badges have one more advanced property that is not present in the UI, `cssFilters`. This accepts a valid CSS `filter` string as defined [here](https://developer.mozilla.org/en-US/docs/Web/CSS/filter). For example, `"cssFilters": "grayscale(100%) brightness(0.8)"` can make an emoji grayscale and slightly dimmer.
## In-Note Location Search & Auto-Complete

View file

@ -1,7 +1,7 @@
{
"id": "obsidian-map-view",
"name": "Map View",
"version": "5.5.0",
"version": "6.0.0",
"minAppVersion": "1.5.6",
"description": "An interactive map view.",
"isDesktopOnly": false

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-map-view",
"version": "5.5.0",
"version": "6.0.0",
"description": "An interactive map view for Obsidian.md",
"main": "main.js",
"type": "module",

View file

@ -20,8 +20,11 @@ export abstract class BaseGeoLayer {
public id: MarkerId;
/** In the case of an inline location, the position within the file where the location was found */
public fileLocation?: number;
/** The leaflet layer on the map; overriden by child classes */
// TODO document, index by container ID
/** The leaflet layer on the map; overriden by child classes.
* For every instance of Map View (specifically: every MapContainer), each logical layer has a different leaflet.Layer object.
* Some layers may not exist in some MapContainers at all (e.g. if their filter does not include them).
* Each map container has an ID, which it can use to find the leaflet.Layer object corresponding to a global BaseGeoLayer.
*/
public abstract geoLayers: Map<number, leaflet.Layer>;
/** In case of an inline location, the line within the file where the geolocation was found */
public fileLine?: number;
@ -79,8 +82,7 @@ export abstract class BaseGeoLayer {
public runDisplayRules(plugin: MapViewPlugin) {}
}
// TODO change class name to LayersMap
export type MarkersMap = Map<MarkerId, BaseGeoLayer>;
export type LayersMap = Map<MarkerId, BaseGeoLayer>;
/**
* Make sure that the coordinates are valid world coordinates

View file

@ -52,7 +52,6 @@
}
function makePreview() {
// TODO unify with the edit dialog
const defaultRule = allRules.find(
(rule: DisplayRule) => rule.preset === true,
);
@ -87,7 +86,7 @@
const queryObject = new Query(app, displayRule.query);
let matches = 0;
for (const layer of plugin.layerCache.map.values()) {
if (queryObject.testMarker(layer)) matches += 1;
if (queryObject.testLayer(layer)) matches += 1;
}
queryError = false;
if (matches === 0) zeroMatchWarning = true;

View file

@ -72,7 +72,7 @@
for (const rule of rules) {
const queryObject = new Query(app, rule.query);
for (const layer of plugin.layerCache.map.values()) {
queryObject.testMarker(layer);
queryObject.testLayer(layer);
}
}
} catch (e) {

View file

@ -327,6 +327,10 @@
font-weight: bold;
}
.setting-item-description {
padding-bottom: 5px;
}
.section {
padding-bottom: 20px;
padding-right: 5px;

View file

@ -87,7 +87,6 @@
if (noteToEdit) {
const headings = app.metadataCache.getFileCache(noteToEdit)
?.headings as HeadingCache[];
// TODO bug here, heading positions change as note changes
allNoteHeadings = headings
? headings.map((heading) => heading.heading)
: [];

View file

@ -35,8 +35,14 @@ export class DisplayRulesCache {
return [iconDetails, pathOptions, badgeOptions];
}
// TODO document
public runOn(marker: BaseGeoLayer): [any, PathOptions, IconBadgeOptions[]] {
/*
* Run the list of display rules on the given layer.
* Starting from the default marker/path options (default icon, path, badge options etc), it iterates over all the rules,
* and any rule that matches the layer overwrites whatever settings that it has in iconDetails and pathOptions,
* and adds additional badge options.
* The result (=the accumulated icon details, accumulated path options and list of badge options) is returned.
*/
public runOn(layer: BaseGeoLayer): [any, PathOptions, IconBadgeOptions[]] {
let [iconDetails, pathOptions, badgeOptions] = this.getDefaults();
if (this.displayRuleQueries.length != this.displayRules.length)
throw new Error(
@ -47,7 +53,7 @@ export class DisplayRulesCache {
// Test & apply the rules one by one, except the preset one, which is always first
if (!rule.preset) {
const query = this.displayRuleQueries[i];
if (query.testMarker(marker)) {
if (query.testLayer(layer)) {
if (rule.iconDetails)
iconDetails = Object.assign(
{},
@ -60,7 +66,7 @@ export class DisplayRulesCache {
pathOptions,
rule.pathOptions,
);
// TODO overwrite existing overlay options with the same overlay
// Possible future improvement: allow adding up properties of badges like in icons and paths.
if (rule.badgeOptions) badgeOptions.push(rule.badgeOptions);
}
}

View file

@ -401,7 +401,7 @@ function addEdgesFromFile(
destination.path,
);
for (let destinationMarker of destinationFileWithMarkers.markers) {
if (isMarkerLinkedFrom(destinationMarker, link, app)) {
if (isLayerLinkedFrom(destinationMarker, link, app)) {
// The link really points to destinationMarker, therefore all the markers in the source file
// are to be linked to this destination marker.
for (let sourceMarker of source.markers) {
@ -425,7 +425,7 @@ function addEdgesFromFile(
* If the link includes a header or a block reference and the marker is an inline marker, 'true' is returned
* only if the marker is in that header/block. A front-matter marker is considered link regardless of the block/header.
*/
export function isMarkerLinkedFrom(
export function isLayerLinkedFrom(
marker: BaseGeoLayer,
linkCache: LinkCache | FrontmatterLinkCache | ReferenceCache,
app: App,

View file

@ -15,6 +15,7 @@ import {
verifyOrAddFrontMatterForInline,
appendToNoteAtHeadingOrEnd,
makeInlineTagsList,
getHeadingAndBlockForFilePosition,
} from 'src/utils';
import { type PluginSettings } from 'src/settings';
import * as regex from 'src/regex';
@ -150,9 +151,9 @@ export async function buildGeoJsonLayers(
if (settings.debug) console.time('buildGeoJsonLayers');
for (const file of files) {
if (file.extension === 'md') {
const fileCache = app.metadataCache.getFileCache(file);
const frontMatter = fileCache?.frontmatter;
if (hasFrontMatterLocations(frontMatter, fileCache, settings)) {
const metadata = app.metadataCache.getFileCache(file);
const frontMatter = metadata?.frontmatter;
if (hasFrontMatterLocations(frontMatter, metadata, settings)) {
// Search for an inline GeoJSON
const content = await app.vault.read(file);
const matches = content.matchAll(regex.INLINE_GEOJSON);
@ -169,7 +170,13 @@ export async function buildGeoJsonLayers(
content
.substring(0, layer.fileLocation)
.split('\n').length - 1;
// TODO add file block and heading
const [heading, block] =
getHeadingAndBlockForFilePosition(
metadata,
layer.fileLocation,
);
layer.fileHeading = heading;
layer.fileBlock = block;
layer.populateMetadata();
layer.generateId();
if (match.groups.tags)

View file

@ -1,7 +1,7 @@
import { BaseGeoLayer, type MarkersMap } from 'src/baseGeoLayer';
import { BaseGeoLayer, type LayersMap } from 'src/baseGeoLayer';
export class LayerCache {
private layersMap: MarkersMap = new Map();
private layersMap: LayersMap = new Map();
private layersByFilePath: Record<string, BaseGeoLayer[]> = {};
/*
@ -29,7 +29,7 @@ export class LayerCache {
return this.layersByFilePath[filePath]?.values() ?? [].values();
}
get map(): MarkersMap {
get map(): LayersMap {
return this.layersMap;
}

View file

@ -65,8 +65,12 @@ import { BaseGeoLayer, cacheTagsFromLayers } from 'src/baseGeoLayer';
export default class MapViewPlugin extends Plugin {
settings: PluginSettings;
// TODO document
// If null it means that the map is not yet initialized.
/*
* This object keeps a single global repository of the layers (markers & paths) that are known to Map View.
* These layers are mapped to files they belong to, and they have a mapping from a container ID to
* "physical" leaflet.Layer objects on the various map containers that are currently active, if any.
* If null, it means that the layer cache was not yet initialized.
*/
public layerCache: LayerCache | null = null;
private layerCacheInitStarted: boolean = false;
public iconFactory: IconFactory;
@ -76,7 +80,7 @@ export default class MapViewPlugin extends Plugin {
private urlConvertor: UrlConvertor;
private mapPreviewPopup: MapPreviewPopup;
public editorLinkReplacePlugin: ViewPlugin<GeoLinkReplacePlugin>;
// Includes all the known tags that are within markers, both inline (which are not necessarily known to Obsidian)
// Includes all the known tags that are within layers, both inline (which are not necessarily known to Obsidian)
// and actual Obsidian tags
public allTags: Set<string>;
private allMapContainers: MapContainer[] = [];
@ -762,7 +766,11 @@ export default class MapViewPlugin extends Plugin {
else return null;
}
// TODO document
/*
* The plugin needs to keep track of which map containers are active.
* This is tricky, and even more when it comes to EmbeddedMap objects, as Obsidian doesn't give any tools to do that
* reliably.
*/
public registerMapContainer(mapContainer: MapContainer) {
this.allMapContainers.push(mapContainer);
}
@ -1191,7 +1199,7 @@ export default class MapViewPlugin extends Plugin {
/**
* Run when a file is deleted, renamed or *changed*.
* When a file is changed, the same is populated in fileRemoved and fileAddedOrChanged, so its markers are removed and then re-added.
* When a file is changed, the same is populated in fileRemoved and fileAddedOrChanged, so its layers are removed and then re-added.
* WARNING: THIS METHOD RUNS A LOT. When a Map View is open and a note is edited anywhere in Obsidian,
* this method can be called repeatedly during typing, and thus must be very efficient to not cause
* delays for the user.
@ -1209,7 +1217,7 @@ export default class MapViewPlugin extends Plugin {
}
let newLayers: BaseGeoLayer[] = [];
if (fileAddedOrChanged && fileAddedOrChanged instanceof TFile) {
// Add file markers from the added/modified file. These markers may be (and usually are) identical
// Add file layers from the added/modified file. These layers may be (and usually are) identical
// to the ones already on the map
newLayers = await buildAndAppendFileMarkers(
[fileAddedOrChanged],
@ -1239,14 +1247,14 @@ export default class MapViewPlugin extends Plugin {
// We want to update each map container about the updated list of layers.
// To do this, we take all its layers, remove the ones that belong to fileRemoved if any, and add
// those from newLayers.
// The mapContainer then compares this list to what it has internally and updates the map markers.
// As an optimization, if we don't remove any layers, we'll use the mapContainer.display.markers object as-is.
// The mapContainer then compares this list to what it has internally and updates the map layers.
// As an optimization, if we don't remove any layers, we'll use the mapContainer.display.layers object as-is.
// If we do, we'll copy it aside, because we don't want to modify the mapContainer's internal structure here.
const existingLayers =
fileRemoved &&
mapContainer.display.markers.hasLayersFromFile(fileRemoved)
? new LayerCache(mapContainer.display.markers)
: mapContainer.display.markers;
mapContainer.display.layers.hasLayersFromFile(fileRemoved)
? new LayerCache(mapContainer.display.layers)
: mapContainer.display.layers;
if (fileRemoved) existingLayers.deleteAllFromFile(fileRemoved);
const newMarkers = mapContainer.filterAndPrepareMarkers(
newLayers,
@ -1257,12 +1265,9 @@ export default class MapViewPlugin extends Plugin {
newMarkers,
);
try {
mapContainer.updateMapMarkers(combinedLayersIterator);
mapContainer.updateMapLayers(combinedLayersIterator);
} catch (e) {
console.error(
'Error updating markers on file modification:',
e,
);
console.error('Error updating layers on file modification:', e);
console.log(
'This happened on container:',
mapContainer,
@ -1277,7 +1282,7 @@ export default class MapViewPlugin extends Plugin {
/*
* What I would have liked to do in the case a file is renamed, is to handle it with the updateMarkersWithRelationToFile
* method above.
* In theory, we could remove the old path and re-add the markers from 'file'.
* In theory, we could remove the old path and re-add the layers from 'file'.
* However, Obsidian puts us in an awkward position: the 'file' object does not yet have metadata built for it, and
* there is no way to register for a metadata change event in the case of a rename (as of 2025-07). Seems like the API
* was built under the assumption that handling rename is straight-forward.
@ -1289,7 +1294,7 @@ export default class MapViewPlugin extends Plugin {
this.layerCache.renameFile(oldPath, file.path);
this.maintainMapContainersList();
for (const mapContainer of this.allMapContainers) {
mapContainer.display.markers.renameFile(oldPath, file.path);
mapContainer.display.layers.renameFile(oldPath, file.path);
}
const fileLayers = this.layerCache.getLayersByFile(file.path);
for (const layer of fileLayers) layer.renameContainingFile(file);
@ -1317,10 +1322,41 @@ export default class MapViewPlugin extends Plugin {
}
}
/*
* Try to find map containers that are no longer used (i.e. not displayed or loaded anywhere) and clean their references
* from the layer maps.
* Since map containers can exist in many forms (full Map View instances, embedded maps, map previews...), there is no
* reliable way that Obsidian provides to track them, so we make a best effort here.
* This method needs to be pretty fast (as it's called from editor updates).
*/
private maintainMapContainersList() {
this.allMapContainers = this.allMapContainers.filter((mapContainer) => {
return document.contains(mapContainer.display.mapDiv);
});
// TODO clear containers from layers
const cleanedContainers = this.allMapContainers.filter(
(mapContainer) => {
return document.contains(mapContainer.display.mapDiv);
},
);
// If containers were removed, remove their references from all geoLayers
if (cleanedContainers.length < this.allMapContainers.length) {
const remainingIds = new Set(
cleanedContainers.map(
(container: MapContainer) => container.containerId,
),
);
const removedContainerIds = this.allMapContainers
.filter((container) => !remainingIds.has(container.containerId))
.map((container) => container.containerId);
if (removedContainerIds.length > 0) {
for (const layer of this.layerCache.layers) {
for (const containerId of removedContainerIds) {
if (layer.geoLayers.has(containerId)) {
layer.geoLayers.delete(containerId);
}
}
}
}
}
this.allMapContainers = cleanedContainers;
}
}

View file

@ -132,9 +132,8 @@ export class MapContainer {
tileLayer: TileLayerOffline;
/** The cluster management class */
clusterGroup: leaflet.MarkerClusterGroup;
/** The markers currently on the map */
// TODO change name
markers: LayerCache = new LayerCache();
/** The layers currently on the map */
layers: LayerCache = new LayerCache();
/** The polylines currently on the map */
polylines: leaflet.Polyline[] = [];
/** The view controls */
@ -190,7 +189,8 @@ export class MapContainer {
private lastTabLeaf: WorkspaceLeaf;
/** Is the view currently open */
private isOpen: boolean = false;
// TODO document
// MapContainer objects have an increasing ID for the purpose of being able to identify themselves in the BaseGeoLayer.geoLayers
// maps
private mapContainerId: number;
private static staticMaxMapContainerId: number = 0;
/** On an embedded map view, this is set by the parent view object so the relevant button can call it. */
@ -255,7 +255,7 @@ export class MapContainer {
}
getMarkers() {
return this.display.markers;
return this.display.layers;
}
isDarkMode(settings: PluginSettings): boolean {
@ -555,7 +555,7 @@ export class MapContainer {
this.display.tileLayer = null;
this.display?.map?.off();
this.display?.map?.remove();
this.display?.markers?.clear();
this.display?.layers?.clear();
this.display?.controls?.reload();
await this.createMap();
this.updateMarkersToState(this.state, true);
@ -796,7 +796,7 @@ export class MapContainer {
);
// This is an ugly work-around for an issue of marker popups sometimes not closing when they should,
// seemingly due to a race condition of closing and opening the popup from multiple markers upon
// seemingly due to a race condition of closing and opening the popup from multiple layers upon
// rapid mouse movements.
// I couldn't find a better solution to this edge case rather than check once in a while if a popup
// happens to be open when it shouldn't.
@ -856,9 +856,9 @@ export class MapContainer {
const forceRecreate = state.markerLabels != this.state.markerLabels;
this.state = structuredClone(state);
// TODO TEMP change names
if (forceRecreate) this.updateMapMarkers([]);
this.updateMapMarkers(allLayers);
// forceRecreate means we want to reset the layers before calling the real updateMapLayers
if (forceRecreate) this.updateMapLayers([]);
this.updateMapLayers(allLayers);
// There are multiple layers of safeguards here, in an attempt to minimize the cases where a series
// of interactions and async updates compete over the map.
// See the comment in internalSetViewState to get more context
@ -907,23 +907,23 @@ export class MapContainer {
let results: BaseGeoLayer[] = [];
const query = new Query(this.app, queryString);
for (const marker of allMarkers)
if (query.testMarker(marker)) results.push(marker);
if (query.testLayer(marker)) results.push(marker);
return results;
}
/**
* Update the actual Leaflet markers of the map according to a new list of logical markers.
* Unchanged markers are not touched, new markers are created and old markers that are not in the updated list are removed.
* Update the actual Leaflet layers of the map according to a new list of logical layers.
* Unchanged layers are not touched, new layers are created and old layers that are not in the updated list are removed.
* Also, all the polylines (edge lines representing links) are cleared and redrawn, which is inefficient and can
* be optimized in the future.
* @param newLayer The new array of layers
*/
updateMapMarkers(newLayers: Iterable<BaseGeoLayer>) {
for (const layer of this.display.markers.layers) layer.touched = false;
updateMapLayers(newLayers: Iterable<BaseGeoLayer>) {
for (const layer of this.display.layers.layers) layer.touched = false;
let layersToAdd: BaseGeoLayer[] = [];
let totalLayers = 0;
for (let layer of newLayers) {
const existingLayer = this.display.markers.get(layer.id);
const existingLayer = this.display.layers.get(layer.id);
if (existingLayer && existingLayer.isSame(layer)) {
// This layer exists, so just keep it
existingLayer.touched = true;
@ -944,11 +944,11 @@ export class MapContainer {
totalLayers++;
}
let layersToRemove: BaseGeoLayer[] = [];
for (const layer of this.display.markers.layers) {
for (const layer of this.display.layers.layers) {
if (!layer.touched) {
layersToRemove.push(layer);
this.display.markers.delete(layer.id);
// Remove the edges that connect the markers we are removing, together with their polylines
this.display.layers.delete(layer.id);
// Remove the edges that connect the layers we are removing, together with their polylines
if (layer instanceof FileMarker)
layer.removeEdges(this.display.polylines);
}
@ -956,31 +956,31 @@ export class MapContainer {
this.display.clusterGroup.removeLayers(
layersToRemove.map((layer) => this.getGeoLayer(layer)),
);
for (const layer of layersToAdd) this.display.markers.add(layer);
// Now add the actual markers & paths on the map
for (const layer of layersToAdd) this.display.layers.add(layer);
// Now add the actual layers & paths on the map
this.display.clusterGroup.addLayers(
layersToAdd.map((layer) => this.getGeoLayer(layer)),
);
this.buildPolylines();
// Just a cheap sanity check
if (totalLayers != this.display.markers.size)
if (totalLayers != this.display.layers.size)
console.error(
'Something went wrong building the map, it has',
this.display.markers.size,
this.display.layers.size,
'items instead of',
totalLayers,
);
}
/**
* Builds all the non-existing polylines according to the edges stored in the markers, and adds them to the map.
* Builds all the non-existing polylines according to the edges stored in the layers, and adds them to the map.
*/
private buildPolylines() {
if (this.state.showLinks) {
for (const marker of this.display.markers.layers) {
if (marker instanceof FileMarker) {
// Draw edges between markers
for (const edge of marker.edges) {
for (const layer of this.display.layers.layers) {
if (layer instanceof FileMarker) {
// Draw edges between layers
for (const edge of layer.edges) {
// Since an edge is linked from both of its sides, we want to make sure we create
// the polyline just once
if (edge.polyline) {
@ -1295,7 +1295,7 @@ export class MapContainer {
event.propagatedFrom.activePopup = content;
}
/** Zoom the map to fit all markers on the screen */
/** Zoom the map to fit all layers on the screen */
public async autoFitMapToMarkers() {
const mapSize = this.display?.map?.getSize();
if (!(mapSize && mapSize.x > 0 && mapSize.y > 0)) {
@ -1305,9 +1305,9 @@ export class MapContainer {
this.autoFitMapToMarkers();
});
}
if (this.display.markers.size > 0) {
if (this.display.layers.size > 0) {
const locations: leaflet.LatLng[] = [];
for (const marker of this.display.markers.layers) {
for (const marker of this.display.layers.layers) {
locations.push(...marker.getBounds());
}
this.display.map.fitBounds(leaflet.latLngBounds(locations), {
@ -1555,7 +1555,7 @@ export class MapContainer {
openSearch() {
if (this.display.searchControls)
this.display.searchControls.openSearch(this.display.markers);
this.display.searchControls.openSearch(this.display.layers);
}
setHighlight(mapOrFileMarker: leaflet.Layer | BaseGeoLayer) {
@ -1601,7 +1601,7 @@ export class MapContainer {
file: TAbstractFile,
fileLine: number | null = null,
): BaseGeoLayer | null {
for (let [_, fileMarker] of this.display.markers.map) {
for (let [_, fileMarker] of this.display.layers.map) {
if (fileMarker.file == file) {
if (!fileLine) return fileMarker;
if (fileLine == fileMarker.fileLine) return fileMarker;
@ -1611,7 +1611,7 @@ export class MapContainer {
}
findMarkerById(markerId: string): BaseGeoLayer | undefined {
return this.display.markers.get(markerId);
return this.display.layers.get(markerId);
}
updateRealTimeLocationMarkers() {
@ -1813,7 +1813,7 @@ export class MapContainer {
startHoverHighlight(markerToFocus: BaseGeoLayer) {
if (!this.state.showLinks) return;
this.display.mapDiv.addClass('mv-fade-active');
for (const marker of this.display.markers.layers) {
for (const marker of this.display.layers.layers) {
if (
markerToFocus &&
marker instanceof FileMarker &&
@ -1831,7 +1831,7 @@ export class MapContainer {
if (element) {
// We add two classes, one denoting that generally a fade is active and another to denote
// that a specific marker should be shown. It is done this way because of cluster groups.
// We want a cluster group to be shown if *at least one* of its markers should be shown
// We want a cluster group to be shown if *at least one* of its layers should be shown
if (shouldBeVisible)
element.addClass('mv-fade-marker-shown');
}
@ -1850,7 +1850,7 @@ export class MapContainer {
endHoverHighlight() {
this.display.mapDiv.removeClass('mv-fade-active');
for (const marker of this.display.markers.layers) {
for (const marker of this.display.layers.layers) {
const geoLayer = this.getGeoLayer(marker);
if (geoLayer && geoLayer instanceof leaflet.Marker) {
const parent =
@ -2029,4 +2029,8 @@ export class MapContainer {
}
}
}
get containerId() {
return this.mapContainerId;
}
}

View file

@ -52,11 +52,12 @@ export function getIconFromOptions(
// Ugly hack for obsidian-leaflet compatability, see https://github.com/esm7/obsidian-map-view/issues/6
// @ts-ignore
const backupL = L;
let icon: leaflet.Icon | leaflet.DivIcon;
try {
// @ts-ignore
L = localL;
if (iconSpec?.shape == 'simple-circle') {
return createSimpleCircleMarker(iconSpec, iconFactory);
icon = createSimpleCircleMarker(iconSpec, iconFactory);
} else {
// We check for iconSpec.icon to allow a custom innerHTML specification for some rules,
// and in such a case, do not wish to override the innerHTML by the icon rendition. See getIconFromRules above
@ -68,30 +69,30 @@ export function getIconFromOptions(
);
iconSpec.innerHTML = internalIcon;
}
return addBadges(
leaflet.ExtraMarkers.icon(
iconSpec as leaflet.ExtraMarkers.IconOptions,
),
badgeOptions,
icon = leaflet.ExtraMarkers.icon(
iconSpec as leaflet.ExtraMarkers.IconOptions,
);
}
} finally {
// @ts-ignore
L = backupL;
}
return addBadges(icon, badgeOptions);
}
function addBadges(
leafletIcon: leaflet.Icon,
leafletIcon: leaflet.Icon | leaflet.DivIcon,
badgeOptions: IconBadgeOptions[],
) {
// TODO support this in simpleCircleMarker too
if (!badgeOptions || badgeOptions.length === 0) return leafletIcon;
const oldCreator = leafletIcon.createIcon;
// The following wraps the createIcon function with one that adds a badge div if such exists.
leafletIcon.createIcon = function (oldIcon: HTMLElement) {
const iconDiv = oldCreator.call(this, oldIcon);
if (iconDiv) {
// TODO document
// We support up to 4 badges, at the 4 corners of the icon, starting from the top left.
// So we iterate over the badges required for this marker and cycle between the badge locations.
// If there are more than 4 badges, the 5th will overwrite the 1st in the top-left corner.
const CLASS_NAMES = [
'mv-badge-tl',
'mv-badge-tr',

View file

@ -33,10 +33,7 @@ export class NoteSelectDialog extends SuggestModal<SuggestInfo> {
this.customOnSelect = action;
this.setPlaceholder(title);
let instructions = [
{ command: 'enter', purpose: 'to use' },
{ command: 'shift+enter', purpose: 'create a new note' },
];
let instructions = [{ command: 'enter', purpose: 'to use' }];
this.setInstructions(instructions);
this.inputEl.addEventListener('keypress', (ev: KeyboardEvent) => {
// In the case of a custom select function, trigger it also for Shift+Enter.
@ -50,7 +47,6 @@ export class NoteSelectDialog extends SuggestModal<SuggestInfo> {
const chooser = (this as any).chooser;
const selectedItem = chooser?.selectedItem;
const values = chooser?.values;
// TODO support create new note
if (chooser && values) {
this.onChooseSuggestion(values[selectedItem], ev);
this.close();

View file

@ -3,8 +3,8 @@ import { App, TFile, TextComponent, PopoverSuggest, Scope } from 'obsidian';
import * as consts from 'src/consts';
import { matchByPosition, getTagUnderCursor } from 'src/utils';
import * as regex from 'src/regex';
import { isLayerLinkedFrom } from 'src/fileMarker';
import { BaseGeoLayer } from 'src/baseGeoLayer';
import { FileMarker, isMarkerLinkedFrom } from 'src/fileMarker';
import * as utils from 'src/utils';
import { checkTagPatternMatch } from 'src/markerIcons';
import MapViewPlugin from 'src/main';
@ -49,7 +49,7 @@ export class Query {
return newString;
}
testMarker(layer: BaseGeoLayer): boolean {
testLayer(layer: BaseGeoLayer): boolean {
if (this.queryEmpty) return true;
const toBool = (s: string) => {
return s === 'true';
@ -85,24 +85,24 @@ export class Query {
return toBool(booleanStack[0]);
}
private testIdentifier(marker: BaseGeoLayer, value: string): boolean {
private testIdentifier(layer: BaseGeoLayer, value: string): boolean {
if (value.startsWith('tag:#')) {
const queryTag = value.replace('tag:', '');
if (queryTag.length === 0) return false;
if (checkTagPatternMatch(queryTag, marker.tags)) return true;
if (checkTagPatternMatch(queryTag, layer.tags)) return true;
return false;
} else if (value.startsWith('name:')) {
const query = value.replace('name:', '').toLowerCase();
if (query.length === 0) return false;
// For inline geolocations, completely ignore the file name and use only the link name
if (marker.extraName)
return marker.extraName.toLowerCase().includes(query);
if (layer.extraName)
return layer.extraName.toLowerCase().includes(query);
// For front matter geolocations, use the file name
return marker.file.name.toLowerCase().includes(query);
return layer.file.name.toLowerCase().includes(query);
} else if (value.startsWith('path:')) {
const queryPath = value.replace('path:', '').toLowerCase();
if (queryPath.length === 0) return false;
return marker.file.path.toLowerCase().includes(queryPath);
return layer.file.path.toLowerCase().includes(queryPath);
} else if (value.startsWith('linkedto:')) {
const query = value.replace('linkedto:', '').toLowerCase();
const linkedToDest = this.app.metadataCache.getFirstLinkpathDest(
@ -110,7 +110,7 @@ export class Query {
'',
);
if (!linkedToDest) return false;
const fileCache = this.app.metadataCache.getFileCache(marker.file);
const fileCache = this.app.metadataCache.getFileCache(layer.file);
const allLinks = [
...(fileCache?.links ?? []),
...(fileCache?.frontmatterLinks ?? []),
@ -139,12 +139,12 @@ export class Query {
...(linksFrom?.frontmatterLinks ?? []),
...(linksFrom?.embeds ?? []),
];
// Check if the given marker is linked from 'fileMatch'
// Check if the given layer is linked from 'fileMatch'
for (const link of allLinks) {
if (isMarkerLinkedFrom(marker, link, this.app)) return true;
if (isLayerLinkedFrom(layer, link, this.app)) return true;
}
// Also include the 'linked from' file itself
if (fileMatch.basename === marker.file.basename) return true;
if (fileMatch.basename === layer.file.basename) return true;
}
} else if (value.startsWith('lines:')) {
const linesQueryMatch = value.match(/(lines:)([0-9]+)-([0-9]+)/);
@ -152,9 +152,9 @@ export class Query {
const fromLine = parseInt(linesQueryMatch[2]);
const toLine = parseInt(linesQueryMatch[3]);
return (
marker.fileLine &&
marker.fileLine >= fromLine &&
marker.fileLine <= toLine
layer.fileLine &&
layer.fileLine >= fromLine &&
layer.fileLine <= toLine
);
}
} else if (value.startsWith('[')) {
@ -166,7 +166,7 @@ export class Query {
const [isExactName, propertyName] = unquote(propertyNameRaw);
const [isExactQuery, propertyQuery] = unquote(propertyQueryRaw);
const fileCache = this.app.metadataCache.getFileCache(marker.file);
const fileCache = this.app.metadataCache.getFileCache(layer.file);
let propertyValues: string[] = [];
if (isExactName) {
const property = fileCache.frontmatter?.[propertyName];

View file

@ -512,7 +512,10 @@ export function getTagUnderCursor(
return matchByPosition(line, regex.TAG_NAME_WITH_HEADER, cursorPosition);
}
// TODO document
// This takes multiple iterables (that can be of different types, e.g. an iterator of a map and an iterator of an array)
// and returns an iterator that will iterate over all the structures without copying them.
// Note that like iterators in general, the result of this function can be used only once, and the iterator will need to be
// re-created if another pass on the combined structure is needed.
export function* combineIterables<T>(...iterables: Iterable<T>[]): Iterable<T> {
for (const iterable of iterables) {
yield* iterable;

View file

@ -71,7 +71,7 @@ a.mv-icon-button.on {
}
.leaflet-container {
background-color: var(--titlebar-background-focused) !important;
background-color: var(--background-primary) !important;
}
.newPresetDialogGrid {

View file

@ -3,7 +3,7 @@
"compilerOptions": {
"types": ["svelte", "node"],
"verbatimModuleSyntax": true,
"strict": false, // TODO fix
"strict": false,
"baseUrl": ".",
"inlineSources": true,
"module": "ESNext",