mirror of
https://github.com/daniel-fiuk/simple-map.git
synced 2026-07-22 06:54:24 +00:00
Final Touches to 1.0.0
This commit is contained in:
parent
0d9df2716e
commit
fdf0b36eb4
7 changed files with 191 additions and 23 deletions
1
src/assets/Pin.svg
Normal file
1
src/assets/Pin.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 325 416" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M162.007,415.437c0,0 -162.007,-87.629 -162.007,-253.429c0,-89.414 72.593,-162.007 162.007,-162.007c89.414,-0 162.007,72.593 162.007,162.007c0,165.8 -162.007,253.429 -162.007,253.429Zm0,-326.316c-40.228,-0 -72.887,32.66 -72.887,72.887c0,40.228 32.66,72.887 72.887,72.887c40.228,0 72.887,-32.66 72.887,-72.887c0,-40.228 -32.66,-72.887 -72.887,-72.887Z"/></svg>
|
||||
|
After Width: | Height: | Size: 809 B |
|
|
@ -1 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 936 1260" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path transform="translate(0, 40)" d="M467.872,1199.768c0,0 -467.872,-253.071 -467.872,-731.896c0,-258.226 209.646,-467.872 467.872,-467.872c258.226,0 467.872,209.646 467.872,467.872c0,478.824 -467.872,731.896 -467.872,731.896Zm0,-942.391c-116.176,0 -210.496,94.32 -210.496,210.496c0,116.176 94.32,210.496 210.496,210.496c116.176,0 210.496,-94.32 210.496,-210.496c0,-116.176 -94.32,-210.496 -210.496,-210.496Z" style="fill:currentColor;"/></svg>
|
||||
|
|
@ -95,7 +95,7 @@ export default class FantasyMap extends Plugin {
|
|||
// load image to infer aspect and tile layout
|
||||
const mapImg = new Image();
|
||||
mapImg.src = mapUrl;
|
||||
mapImg.onload = () => {
|
||||
mapImg.onload = async () => {
|
||||
const ratio = mapImg.naturalHeight / mapImg.naturalWidth;
|
||||
(mapWrapper as any)._mapAspectRatio = ratio;
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ export default class FantasyMap extends Plugin {
|
|||
const zoomInput = toolbar.createEl("input", {
|
||||
cls: "fm-zoom-step",
|
||||
type: "number",
|
||||
}) as HTMLInputElement;
|
||||
});
|
||||
zoomInput.value = String(parameters.defaultZoomIncrement ?? this.settings.defaultZoomIncrement);
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { FantasyMapSettings } from "./settings";
|
|||
export interface FantasyMapParams {
|
||||
map: string;
|
||||
mapIDs: string[];
|
||||
pinSize: string;
|
||||
defaultZoomIncrement?: number;
|
||||
defaultZoomLevel?: number;
|
||||
defaultLocation?: [number, number];
|
||||
|
|
@ -19,6 +20,7 @@ export interface FantasyMapParams {
|
|||
// Help message strings for each parameter, which will be displayed when the user includes a help keyword in the value of any parameter; these messages provide detailed instructions on how to use each parameter and what values are accepted
|
||||
const mapHelpMessageString = "**\"Map\"** (**required**): The name of the map asset to display - supports relative paths, absolute paths, and bare filenames (e.g. \"World Map.svg\") - the file must be located somewhere in your vault - supported file types are SVG, PNG, JPG, JPEG, WEBP, and GIF"
|
||||
const mapIDsHelpMessageString = "**\"Map IDs\"** (optional): A comma-separated list of IDs that define which pins with the same ID can appear on the map (e.g. \"river, mountain\"); if not specified, all pins will be displayed on the map regardless of their ID";
|
||||
const pinSizeHelpMessageString = "**\"Pin Size\"** (optional): The size of the pins on the map. Specificaly the width of the pin element, (e.g. \"24px\", \"1.5em\", etc.) – default: \"24px\", can be adjusted in the plugin settings window";
|
||||
const defaultZoomIncrementHelpMessageString = "**\"Default Zoom Increment\"** (optional): The zoom increment amount – default: \"1\", can be adjusted in the plugin settings window";
|
||||
const defaultZoomLevelHelpMessageString = "**\"Default Zoom Level\"** (optional): The initial zoom level when the map is first loaded – default: \"1\"";
|
||||
const defaultLocationHelpMessageString = "**\"Default Location\"** (optional): The initial focused coordinates for the map on load \"(latitude, longitude)\" – default: \"(0, 0)\"";
|
||||
|
|
@ -48,7 +50,7 @@ function allHelpMessages(settings: FantasyMapSettings) : string[] {
|
|||
}
|
||||
|
||||
// This string provides a template for users to copy and paste into their notes to create a new Fantasy-Map code block with all available parameters listed for easy reference
|
||||
export const fantasyMapCopyToClipboardString = [
|
||||
export const fantasyMapCodeBlockCopyToClipboardString = [
|
||||
"```Fantasy-Map",
|
||||
"Map:",
|
||||
"MapIDs:",
|
||||
|
|
@ -64,6 +66,14 @@ export const fantasyMapCopyToClipboardString = [
|
|||
"```"
|
||||
].join("\n");
|
||||
|
||||
export const fantasyMapFrontMatterCopyToClipboardString = [
|
||||
"---",
|
||||
"fm-location: (lat, lng)",
|
||||
"fm-id:",
|
||||
"fm-pin-icon:",
|
||||
"---"
|
||||
].join("\n");
|
||||
|
||||
// Comprehensive list of affirmative values that will be interpreted as true for boolean parameters like "repeat"; this allows for a wide range of user inputs to enable the feature without requiring strict formatting
|
||||
const affirmatives = [
|
||||
"yes", "y",
|
||||
|
|
@ -168,7 +178,7 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx:
|
|||
// help message for the map parameter if the value contains any help keywords
|
||||
if (checkForHelp(mapHelpMessageString)) break;
|
||||
|
||||
if (value.trim().length == 0) addHelpMessage("<span class=\"fantasy-map-error\">Fantasy Map ERROR: map option is required!</span>") ;
|
||||
if (value.trim().length == 0) addHelpMessage("<span class=\"fantasy-map-error\">Fantasy Map ERROR: map option is required!</span>");
|
||||
|
||||
// set the name of the map to load; supports relative paths, absolute paths, and bare filenames (e.g. "World Map.svg") - the file must be located somewhere in your vault - supported file types are SVG, PNG, JPG, JPEG, WEBP, and GIF
|
||||
params.map = value;
|
||||
|
|
@ -183,6 +193,15 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx:
|
|||
params.mapIDs = value.split(",").map(id => id.trim());
|
||||
break;
|
||||
|
||||
case "pinsize":
|
||||
if (checkForHelp(pinSizeHelpMessageString)) break;
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!isValidPinSize(trimmed)) addHelpMessage("<span class=\"fantasy-map-error\">Fantasy Map ERROR: Pin Size Value is invalid! Some examples of acceptable values are '24px', '5%'. This option utalizes css styles, specificaly the width property. </span>")
|
||||
|
||||
params.pinSize = value;
|
||||
break;
|
||||
|
||||
// set the default zoom increment, defaulting to 1 if the value is not a valid number
|
||||
case "defaultzoomincrement":
|
||||
|
||||
|
|
@ -297,6 +316,7 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx:
|
|||
}
|
||||
|
||||
// Defaults
|
||||
if (params.pinSize == null) params.pinSize = settings.defaultPinSize;
|
||||
if (params.defaultZoomIncrement == null) params.defaultZoomIncrement = settings.defaultZoomIncrement;
|
||||
if (params.defaultZoomLevel == null) params.defaultZoomLevel = 1;
|
||||
if (params.defaultLocation == null) params.defaultLocation = [0, 0];
|
||||
|
|
@ -312,6 +332,19 @@ export function parseFantasyMapParams(source: string, element: HTMLElement, ctx:
|
|||
return params as FantasyMapParams;
|
||||
}
|
||||
|
||||
function isValidPinSize(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.style.width = "";
|
||||
el.style.width = trimmed;
|
||||
|
||||
const result = el.style.width !== "";
|
||||
el.remove();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Render a help message to markdown
|
||||
export async function fantasyMapHelpMessage(element: HTMLElement, ctx: MarkdownPostProcessorContext, component: Component, message: string | null, includeCopyLink: boolean = false) {
|
||||
|
||||
|
|
@ -330,7 +363,7 @@ export function appendCopyLink(container: HTMLElement) {
|
|||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await navigator.clipboard.writeText(fantasyMapCopyToClipboardString);
|
||||
await navigator.clipboard.writeText(fantasyMapCodeBlockCopyToClipboardString);
|
||||
link.setText("Copied!");
|
||||
setTimeout(() => link.setText("Copy Fantasy-Map template"), 1500);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import {App, CachedMetadata, MarkdownPostProcessorContext, MarkdownRenderer, Plugin, TFile, Component } from "obsidian";
|
||||
import {App, CachedMetadata, MarkdownPostProcessorContext, MarkdownRenderer, Plugin, normalizePath, TFile, Component } from "obsidian";
|
||||
import { FantasyMapSettings } from "./settings";
|
||||
import { FantasyMapParams } from "./paramaters";
|
||||
import { cancelMapPanning } from "./mapInteractions";
|
||||
import { showCustomPreview, hideCustomPreview, destroyCustomPreview } from "./previewInteractions"
|
||||
|
||||
import pinIcon from "./assets/mapPinIcon_customizable.svg";
|
||||
import defaultPinIcon from "./assets/Pin.svg";
|
||||
|
||||
export interface Pin {
|
||||
note: TFile;
|
||||
|
|
@ -50,7 +50,7 @@ export async function initPinInteractions(
|
|||
createPin(note);
|
||||
}
|
||||
|
||||
function createPin(note: TFile){
|
||||
async function createPin(note: TFile){
|
||||
//#region parse and filter note front matter
|
||||
|
||||
// get the note front matter; if no front matter detected, skip the note
|
||||
|
|
@ -58,14 +58,14 @@ export async function initPinInteractions(
|
|||
const frontMatter = cache?.frontmatter;
|
||||
if (!frontMatter) return;
|
||||
|
||||
// if mapIDs parameter is set, only include notes with a matching fantasy-map-id in their front matter
|
||||
// if mapIDs parameter is set, only include notes with a matching fm-id in their front matter
|
||||
if (paramaters.mapIDs.length > 0 && paramaters.mapIDs[0] !== "") {
|
||||
const mapId = frontMatter["fantasy-map-id"];
|
||||
const mapId = frontMatter["fm-id"];
|
||||
if (mapId !== undefined && !paramaters.mapIDs.includes(mapId)) return;
|
||||
}
|
||||
|
||||
// get location from front matter and parse it; if no location or invalid format, skip the note
|
||||
const frontMatterLc = frontMatter["fantasy-map-location"];
|
||||
const frontMatterLc = frontMatter["fm-location"];
|
||||
if (frontMatterLc === undefined) return;
|
||||
|
||||
const location = parseFormattedLocation(frontMatterLc);
|
||||
|
|
@ -88,8 +88,26 @@ export async function initPinInteractions(
|
|||
|
||||
// get the pin icon and add it to the pin element; set the width of the icon to 12px
|
||||
const pinIconEl = element.createEl("div", { cls: "map-pin-icon" });
|
||||
pinIconEl.innerHTML = pinIcon;
|
||||
pinIconEl.style.width = `12px`;
|
||||
|
||||
const pinIconValue = frontMatter["fm-pin-icon"];
|
||||
const pinFile = resolvePinIconFile(app, pinIconValue, ctx.sourcePath);
|
||||
|
||||
const color = "--link-color";
|
||||
|
||||
if (pinFile) {
|
||||
if (pinFile.extension === "svg") {
|
||||
pinIconEl.innerHTML = addSvgViewBoxPadding(await app.vault.cachedRead(pinFile), 2);
|
||||
} else {
|
||||
const url = app.vault.getResourcePath(pinFile);
|
||||
pinIconEl.innerHTML = `<img src="${url}" alt="" />`;
|
||||
}
|
||||
} else {
|
||||
pinIconEl.innerHTML = addSvgViewBoxPadding(defaultPinIcon, 2);
|
||||
}
|
||||
|
||||
const match = paramaters.pinSize.trim().match(/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))%$/);
|
||||
if (match) pinIconEl.style.width = `${Number(match[1]) * 0.01 * currentMap.width}px`;
|
||||
else pinIconEl.style.width = paramaters.pinSize;
|
||||
//#endregion
|
||||
|
||||
const newPin = { note, element, location }
|
||||
|
|
@ -125,7 +143,7 @@ export async function initPinInteractions(
|
|||
selectedPin.location = pxToLocation(px);
|
||||
await app.fileManager.processFrontMatter(selectedPin.note, (frontmatter) => {
|
||||
if (!selectedPin) return;
|
||||
frontmatter["fantasy-map-location"] = formatLocation(selectedPin?.location);
|
||||
frontmatter["fm-location"] = formatLocation(selectedPin?.location);
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
|
@ -148,6 +166,54 @@ export async function initPinInteractions(
|
|||
})
|
||||
}
|
||||
|
||||
function resolvePinIconFile(
|
||||
app: App,
|
||||
value: unknown,
|
||||
sourcePath: string
|
||||
): TFile | null {
|
||||
if (typeof value !== "string") return null;
|
||||
|
||||
const raw = value.trim();
|
||||
if (!raw) return null;
|
||||
|
||||
const normalized = raw.replace(/^\[\[|\]\]$/g, "").trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
const linked = app.metadataCache.getFirstLinkpathDest(normalized, sourcePath);
|
||||
if (linked instanceof TFile) return linked;
|
||||
|
||||
const byPath = app.vault.getAbstractFileByPath(normalizePath(normalized));
|
||||
if (byPath instanceof TFile) return byPath;
|
||||
|
||||
const files = app.vault.getFiles();
|
||||
const lower = normalized.toLowerCase();
|
||||
|
||||
const supportedExts = new Set(["svg", "png", "jpg", "jpeg", "webp", "gif"]);
|
||||
|
||||
const exactName = files.find(f => f.name.toLowerCase() === lower);
|
||||
if (exactName) return exactName;
|
||||
|
||||
const basenameMatches = files.filter(
|
||||
f => f.basename.toLowerCase() === lower && supportedExts.has(f.extension.toLowerCase())
|
||||
);
|
||||
|
||||
if (basenameMatches.length === 1) return basenameMatches[0] as TFile;
|
||||
|
||||
// Optional: prefer SVG if multiple matches exist
|
||||
const svgMatch = basenameMatches.find(f => f.extension.toLowerCase() === "svg");
|
||||
if (svgMatch) return svgMatch;
|
||||
|
||||
return basenameMatches[0] ?? null;
|
||||
}
|
||||
|
||||
function addSvgViewBoxPadding(svg: string, pad: number): string {
|
||||
return svg.replace(/viewBox="([^"]+)"/, (_, vb) => {
|
||||
const [x, y, w, h] = vb.trim().split(/\s+/).map(Number);
|
||||
if ([x, y, w, h].some(Number.isNaN)) return `viewBox="${vb}"`;
|
||||
return `viewBox="${x - pad} ${y - pad} ${w + pad * 2} ${h + pad * 2}"`;
|
||||
});
|
||||
}
|
||||
|
||||
//#region Helper functions for translating between location and px coordinates, and handling mouse position and hover timeouts
|
||||
|
||||
interface Location { lat: number; lng: number }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import {App, PluginSettingTab, Setting} from "obsidian";
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import FantasyMap from "./main";
|
||||
import {fantasyMapCopyToClipboardString} from "./paramaters";
|
||||
import { fantasyMapCodeBlockCopyToClipboardString, fantasyMapFrontMatterCopyToClipboardString } from "./paramaters";
|
||||
|
||||
export interface FantasyMapSettings {
|
||||
defaultPinSize: string; // e.g. "24px", "1.5rem", "5%"
|
||||
defaultUnitOfMeasurement: string; // Metric or Imperial
|
||||
defaultZoomIncrement: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: FantasyMapSettings = {
|
||||
defaultUnitOfMeasurement: "Metric",
|
||||
defaultZoomIncrement: 1
|
||||
defaultPinSize: "24px",
|
||||
defaultZoomIncrement: 1,
|
||||
defaultUnitOfMeasurement: "Metric"
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
|
@ -38,6 +40,46 @@ export class FantasyMapSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.defaultUnitOfMeasurement = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));*/
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default Pin Size")
|
||||
.setDesc("Set the default size for pins on the map (e.g. 24px, 1.5rem, 5%).")
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder("Enter a size (e.g. 24px)")
|
||||
.setValue(this.plugin.settings.defaultPinSize)
|
||||
.onChange(async (value) => {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!isValidPinSize(trimmed)) {
|
||||
text.inputEl.addClass("is-invalid");
|
||||
text.inputEl.setAttribute(
|
||||
"title",
|
||||
"Enter a valid CSS size like 24px, 1.5rem, 5%, 10vw, or 0."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
text.inputEl.removeClass("is-invalid");
|
||||
text.inputEl.removeAttribute("title");
|
||||
|
||||
this.plugin.settings.defaultPinSize = trimmed;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
function isValidPinSize(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.style.width = "";
|
||||
el.style.width = trimmed;
|
||||
|
||||
const result = el.style.width !== "";
|
||||
el.remove();
|
||||
return result;
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default Zoom Increment')
|
||||
|
|
@ -61,7 +103,7 @@ export class FantasyMapSettingTab extends PluginSettingTab {
|
|||
.setName("Template code block")
|
||||
.setDesc("Copy this into a note to start a Fantasy-Map block.")
|
||||
.addTextArea(text => {
|
||||
text.setValue(fantasyMapCopyToClipboardString);
|
||||
text.setValue(fantasyMapCodeBlockCopyToClipboardString);
|
||||
text.setDisabled(true);
|
||||
text.inputEl.rows = 11; // adjust to fit your block
|
||||
text.inputEl.style.width = "100%";
|
||||
|
|
@ -73,7 +115,34 @@ export class FantasyMapSettingTab extends PluginSettingTab {
|
|||
.setCta()
|
||||
.onClick(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(fantasyMapCopyToClipboardString);
|
||||
await navigator.clipboard.writeText(fantasyMapCodeBlockCopyToClipboardString);
|
||||
// @ts-ignore
|
||||
new Notice("Fantasy-Map block copied");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// @ts-ignore
|
||||
new Notice("Failed to copy block");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Template Note Front Matter")
|
||||
.setDesc("Copy this into a note to pin a note to your map.")
|
||||
.addTextArea(text => {
|
||||
text.setValue(fantasyMapFrontMatterCopyToClipboardString);
|
||||
text.setDisabled(true);
|
||||
text.inputEl.rows = 11; // adjust to fit your block
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.fontFamily = "var(--font-monospace)";
|
||||
})
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText("Copy")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(fantasyMapFrontMatterCopyToClipboardString);
|
||||
// @ts-ignore
|
||||
new Notice("Fantasy-Map block copied");
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -136,14 +136,14 @@
|
|||
transform-origin: center bottom;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 120ms ease, filter 120ms ease;
|
||||
color: var(--interactive-normal);
|
||||
fill: var(--text-accent);
|
||||
}
|
||||
|
||||
.map-pin-icon svg:hover {
|
||||
cursor: pointer;
|
||||
transform: translate(-100%, -10%) scale(1.1);
|
||||
filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));
|
||||
color: var(--interactive-hover);
|
||||
fill: var(--text-accent-hover);
|
||||
}
|
||||
|
||||
/* endregion */
|
||||
|
|
|
|||
Loading…
Reference in a new issue