Add more shapes (#78)

pull/147/head
Candid Dauth 2021-04-10 21:59:20 +02:00
rodzic 8a557b19c2
commit 699a660a65
5 zmienionych plików z 124 dodań i 65 usunięć

Wyświetl plik

@ -35,7 +35,7 @@ To edit the details of a marker, select the marker and then click “Edit data
* **Colour:** The colour of the marker.
* **Size:** The size of the marker (height in pixels). Use the + and − buttons to change the value.
* **Icon:** An icon to show inside the marker shape. You can leave it empty (will show a dot), select an icon from the list, or type in a single character (such as `1`).
* **Shape:** The shape of the marker, either a drop or a circle.
* **Shape:** The shape of the marker.
* **Description:** The description of the marker. Will be shown in the marker details. You can format the text using [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
Click “Save” to save your changes.

Wyświetl plik

@ -13,7 +13,7 @@
padding: 0;
list-style-type: none;
display: grid;
grid-template-columns: repeat(2, 37px);
grid-template-columns: repeat(5, 37px);
overflow-y: auto;
li {

Wyświetl plik

@ -36,10 +36,10 @@
}
createIcons("icons", (icon) => L.FacilMap.getSymbolHtml("#000000", 50, icon));
createIcons("icons-circle", (icon) => L.FacilMap.getMarkerHtml("#ccffcc", 50, icon, "circle"));
createIcons("icons-circle-highlight", (icon) => L.FacilMap.getMarkerHtml("#ccffcc", 50, icon, "circle", true));
createIcons("icons-drop", (icon) => L.FacilMap.getMarkerHtml("#ccffcc", 50, icon, "drop"));
createIcons("icons-drop-highlight", (icon) => L.FacilMap.getMarkerHtml("#ccffcc", 50, icon, "drop", true));
for (const shape of ["drop", "rectangle-marker", "circle", "rectangle", "diamond", "pentagon", "hexagon", "triangle", "triangle-down", "star"]) {
createIcons(`icons-${shape}`, (icon) => L.FacilMap.getMarkerHtml("#ccffcc", 50, icon, shape));
createIcons(`icons-${shape}-highlight`, (icon) => L.FacilMap.getMarkerHtml("#ccffcc", 50, icon, shape, true));
}
</script>
</body>
</html>

Wyświetl plik

@ -1,4 +1,4 @@
import { Colour, Shape, Symbol } from "facilmap-types";
import { Shape, Symbol } from "facilmap-types";
import { makeTextColour, quoteHtml } from "facilmap-utils";
import L, { Icon } from "leaflet";
import { memoize } from "lodash";
@ -24,49 +24,110 @@ export const symbolList = Object.keys(rawIcons).map((key) => Object.keys(rawIcon
export const RAINBOW_STOPS = `<stop offset="0" stop-color="red"/><stop offset="33%" stop-color="#ff0"/><stop offset="50%" stop-color="#0f0"/><stop offset="67%" stop-color="cyan"/><stop offset="100%" stop-color="blue"/>`;
interface ShapeInfo {
svg: string;
highlightSvg: string;
height: number;
/** An SVG path that defines the shape of the marker and has a height of 36px. */
path: string;
/** The width of the SVG path. */
width: number;
baseX: number;
baseY: number;
/** The X and Y coordinates of the point on the marker that should be aligned with the point on the map.
* For a square marker, the bottom center would be [36, 18] and the center would be [18, 18]. */
base: [number, number];
/** The X and Y coordinates of the center of the marker. This is where the symbol will be inserted.
* For a square marker, the center would be [18, 18]. */
center: [number, number];
/** The height/width that inserted symbols should have. */
symbolSize: number;
/** Scale factor for the shape itself. If this is 1, a markers that has its size set to 25 will be 25px high. */
scale: number;
}
const MARKER_SHAPES: Partial<Record<Shape, ShapeInfo>> = {
drop: {
svg: (
`<path id="drop-%ID%" style="stroke: %BORDER_COLOUR%; stroke-width: 2; stroke-linecap: round; fill: %COLOUR%; clip-path: url(#clip-%ID%)" d="M13 0C4.727 0 0 4.8 0 13.2 0 21.6 11.744 36 13 36c1.256 0 13-14.4 13-22.8S21.273 0 13 0z"/>"/>` +
`<clipPath id="clip-%ID%"><use xlink:href="#drop-%ID%"/></clipPath>` + // Don't increase the size by increasing the border: https://stackoverflow.com/a/32162431/242365
`<g transform="translate(4, 4) scale(0.75)">%SYMBOL%</g>`
),
highlightSvg: (
`<path id="drop-%ID%" style="stroke: %BORDER_COLOUR%; stroke-width: 6; stroke-linecap: round; fill: %COLOUR%; clip-path: url(#clip-%ID%)" d="M13 0C4.727 0 0 4.8 0 13.2 0 21.6 11.744 36 13 36c1.256 0 13-14.4 13-22.8S21.273 0 13 0z"/>"/>` +
`<clipPath id="clip-%ID%"><use xlink:href="#drop-%ID%"/></clipPath>` +
`<g transform="translate(4, 4) scale(0.75)">%SYMBOL%</g>`
),
height: 36,
const SHAPE_HEIGHT = 36;
const DEFAULT_SHAPE: Shape = "drop";
const MARKER_SHAPES: Record<Shape, ShapeInfo> = {
"drop": {
path: "M13 0C4.727 0 0 4.8 0 13.2 0 21.6 11.744 36 13 36c1.256 0 13-14.4 13-22.8S21.273 0 13 0z",
width: 26,
baseX: 13,
baseY: 36,
base: [13, 36],
center: [13, 13],
symbolSize: 18,
scale: 1
},
circle: {
svg: (
`<circle id="circle-%ID%" style="stroke: %BORDER_COLOUR%; stroke-width: 2; fill: %COLOUR%; clip-path: url(#clip-%ID%)" cx="18" cy="18" r="18" />` +
`<clipPath id="clip-%ID%"><use xlink:href="#circle-%ID%"/></clipPath>` +
`<g transform="translate(6, 6)">%SYMBOL%</g>`
),
highlightSvg: (
`<circle id="circle-%ID%" style="stroke: %BORDER_COLOUR%; stroke-width: 6; fill: %COLOUR%; clip-path: url(#clip-%ID%)" cx="18" cy="18" r="18" />` +
`<clipPath id="clip-%ID%"><use xlink:href="#circle-%ID%"/></clipPath>` +
`<g transform="translate(6, 6)">%SYMBOL%</g>`
),
height: 36,
"rectangle-marker": {
path: "M2.64 0A2.646 2.646 0 0 0 0 2.646v24.708A2.646 2.646 0 0 0 2.646 30h8.89l1.732 3L15 36l1.732-3 1.732-3h8.89A2.646 2.646 0 0 0 30 27.354V2.646A2.646 2.646 0 0 0 27.354 0H2.646a2.646 2.646 0 0 0-.005 0z",
width: 30,
base: [15, 36],
center: [15, 15],
symbolSize: 24,
scale: 0.95
},
"circle": {
path: "M36 18a18 18 0 0 1-18 18A18 18 0 0 1 0 18 18 18 0 0 1 18 0a18 18 0 0 1 18 18z",
width: 36,
baseX: 18,
baseY: 18,
base: [18, 18],
center: [18, 18],
symbolSize: 24,
scale: 0.85
},
"rectangle": {
path: "M2.646 0h30.708A2.646 2.646 45 0 1 36 2.646v30.708A2.646 2.646 135 0 1 33.354 36H2.646A2.646 2.646 45 0 1 0 33.354V2.646A2.646 2.646 135 0 1 2.646 0z",
width: 36,
base: [18, 18],
center: [18, 18],
symbolSize: 28,
scale: 0.8
},
"diamond": {
path: "M19.382.573l16.045 16.045a1.955 1.955 90 0 1 0 2.764L19.382 35.427a1.955 1.955 180 0 1-2.764 0L.573 19.382a1.955 1.955 90 0 1 0-2.764L16.618.573a1.955 1.955 0 0 1 2.764 0z",
width: 36,
base: [18, 18],
center: [18, 18],
symbolSize: 18,
scale: 0.9
},
"pentagon": {
path: "M20.078.347l17.17 12.371a1.808 1.808 71.97 0 1 .662 2.03l-6.562 19.995A1.826 1.826 144.093 0 1 29.612 36l-21.237-.006a1.826 1.826 35.94 0 1-1.735-1.258L.09 14.738a1.808 1.808 108.063 0 1 .662-2.03L17.93.346a1.837 1.837.017 0 1 2.148 0z",
width: 38,
base: [19, 20],
center: [19, 20],
symbolSize: 20,
scale: 0.85
},
"hexagon": {
path: "M14.833 35.692L1.151 27.826A2.305 2.29 0 0 1 0 25.842l.013-15.71A2.305 2.29 0 0 1 1.167 8.15L14.862.306a2.305 2.29 0 0 1 2.305.002l13.682 7.866A2.305 2.29 0 0 1 32 10.158l-.013 15.71a2.305 2.29 0 0 1-1.154 1.982l-13.695 7.844a2.305 2.29 0 0 1-2.305-.002z",
width: 32,
base: [16, 18],
center: [16, 18],
symbolSize: 22,
scale: 0.9
},
"triangle": {
path: "M.134 34.33L19.976.494a1.013 1.013 179.88 0 1 1.746-.003l20.139 34.014a.986.986 119.818 0 1-.853 1.489L.977 35.809a.982.982 60.327 0 1-.843-1.48z",
width: 42,
base: [21, 24],
center: [21, 24],
symbolSize: 16,
scale: 0.85
},
"triangle-down": {
path: "M.934 0a.96.96 0 0 0-.805 1.44l19.036 33.12.004.007.824 1.433.828-1.44 19.05-33.112a.96.96 0 0 0-.828-1.44L.958.001H.934z",
width: 40,
base: [20, 36],
center: [20, 12],
symbolSize: 16,
scale: 0.85
},
"star": {
path: "M19.007 0l5.865 11.851L38 13.758l-9.505 9.218L30.732 36l-11.74-6.154-11.746 6.142 2.25-13.021L0 13.739l13.13-1.893z",
width: 38,
base: [19, 18],
center: [19, 20],
symbolSize: 14,
scale: 0.9
}
};
@ -149,52 +210,50 @@ let idCounter = 0;
export function getMarkerCode(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): string {
const borderColour = makeTextColour(colour, 0.3);
const id = `${idCounter++}`;
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES.drop!;
const shapeCode = (highlight ? shapeObj.highlightSvg : shapeObj.svg)
.replace(/%BORDER_COLOUR%/g, borderColour)
.replace(/%COLOUR%/g, colour == "rainbow" ? `url(#fm-rainbow-${id})` : colour)
.replace(/%SYMBOL%/g, getSymbolCode(borderColour, 24, symbol))
.replace(/%ID%/g, id);
const scale = height / shapeObj.height;
const colourCode = colour == "rainbow" ? `url(#fm-rainbow-${id})` : colour;
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
const symbolCode = getSymbolCode(borderColour, shapeObj.symbolSize, symbol);
const translateX = `${Math.floor(shapeObj.center[0] - shapeObj.symbolSize / 2)}`;
const translateY = `${Math.floor(shapeObj.center[1] - shapeObj.symbolSize / 2)}`;
return (
`<g transform="scale(${scale})">` +
`<g transform="scale(${height / SHAPE_HEIGHT})">` +
(colour == "rainbow" ? `<defs><linearGradient id="fm-rainbow-${id}" x2="0" y2="100%">${RAINBOW_STOPS}</linearGradient></defs>` : '') +
shapeCode +
`<path id="shape-${id}" style="stroke: ${borderColour}; stroke-width: ${highlight ? 6 : 2}; stroke-linecap: round; fill: ${colourCode}; clip-path: url(#clip-${id})" d="${quoteHtml(shapeObj.path)}"/>"/>` +
`<clipPath id="clip-${id}"><use xlink:href="#shape-${id}"/></clipPath>` + // Don't increase the size by increasing the border: https://stackoverflow.com/a/32162431/242365
`<g transform="translate(${translateX}, ${translateY})">${symbolCode}</g>` +
`</g>`
);
}
export function getMarkerUrl(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): string {
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES.drop!;
const width = Math.ceil(height * shapeObj.width / shapeObj.height);
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
const width = Math.ceil(height * shapeObj.width / SHAPE_HEIGHT);
return "data:image/svg+xml,"+encodeURIComponent(
`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` +
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${shapeObj.width} ${shapeObj.height}" version="1.1">` +
getMarkerCode(colour, shapeObj.height, symbol, shape, highlight) +
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${shapeObj.width} ${SHAPE_HEIGHT}" version="1.1">` +
getMarkerCode(colour, SHAPE_HEIGHT, symbol, shape, highlight) +
`</svg>`
);
}
export function getMarkerHtml(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): string {
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES.drop!;
const width = Math.ceil(height * shapeObj.width / shapeObj.height);
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
const width = Math.ceil(height * shapeObj.width / SHAPE_HEIGHT);
return (
`<svg width="${width}" height="${height}" viewBox="0 0 ${shapeObj.width} ${shapeObj.height}">` +
getMarkerCode(colour, shapeObj.height, symbol, shape, highlight) +
`<svg width="${width}" height="${height}" viewBox="0 0 ${shapeObj.width} ${SHAPE_HEIGHT}">` +
getMarkerCode(colour, SHAPE_HEIGHT, symbol, shape, highlight) +
`</svg>`
);
}
export function getMarkerIcon(colour: string, height: number, symbol?: Symbol, shape?: Shape, highlight = false): Icon {
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES.drop!;
const scale = shapeObj.scale * height / shapeObj.height;
const shapeObj = (shape && MARKER_SHAPES[shape]) || MARKER_SHAPES[DEFAULT_SHAPE];
const scale = shapeObj.scale * height / SHAPE_HEIGHT;
return L.icon({
iconUrl: getMarkerUrl(colour, height, symbol, shape, highlight),
iconSize: [shapeObj.width*scale, shapeObj.height*scale],
iconAnchor: [Math.round(shapeObj.baseX*scale), Math.round(shapeObj.baseY*scale)],
iconSize: [Math.round(shapeObj.width*scale), Math.round(SHAPE_HEIGHT*scale)],
iconAnchor: [Math.round(shapeObj.base[0]*scale), Math.round(shapeObj.base[1]*scale)],
popupAnchor: [0, -height]
});
}

Wyświetl plik

@ -4,7 +4,7 @@ export type ZoomLevel = number;
/** Colour in 6-digit hex format without a # */
export type Colour = string;
export type Symbol = string;
export type Shape = "" | "drop" | "circle";
export type Shape = string;
export type ID = number;
export type RouteMode = string;
export type Layer = string;