leaflet-extra-markers
Version:
Custom map markers for Leaflet JS
319 lines (283 loc) • 9.82 kB
JavaScript
import { Browser, Icon as IconBase, Point, Util } from "leaflet";
import { PinTeardropBorder } from "./markers/pin-teardrop-border.js";
import { createElement, createSvgElement } from "./util.js";
const shadowCast =
"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='39' height='36' fill='currentColor' viewBox='0 0 39 36'%3e %3cg filter='url(%23a)'%3e %3cpath fill='url(%23b)' d='M25 4.34c7.3.76 11.47 6.93 9.54 12.27a9.99 9.99 0 0 1-3.9 4.77L15.92 31.8a1.2 1.2 0 0 1-.93.19c-.34-.07-.6-.27-.68-.5L12 16.97c-.39-2 .12-4.76 1.08-6.77C15.64 5.96 18.16 3.63 25 4.34Z'/%3e %3c/g%3e %3cdefs%3e %3clinearGradient id='b' x1='27' x2='14.75' y1='6' y2='32.33' gradientUnits='userSpaceOnUse'%3e %3cstop stop-opacity='0'/%3e %3cstop offset='1' stop-opacity='.5'/%3e %3c/linearGradient%3e %3cfilter id='a' width='31.14' height='35.78' x='7.87' y='.22' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse'%3e %3cfeFlood flood-opacity='0' result='BackgroundImageFix'/%3e %3cfeBlend in='SourceGraphic' in2='BackgroundImageFix' result='shape'/%3e %3cfeGaussianBlur result='effect1_foregroundBlur_53_1294' stdDeviation='2'/%3e %3c/filter%3e %3c/defs%3e %3c/svg%3e";
const shadowEllipse =
"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='6' fill='currentColor' viewBox='0 0 30 6'%3e %3cellipse cx='15' cy='3' fill='url(%23a)' rx='10' ry='3'/%3e %3cdefs%3e %3cradialGradient id='a' cx='0' cy='0' r='1' gradientTransform='matrix(0 3 -10 0 15 3)' gradientUnits='userSpaceOnUse'%3e %3cstop offset='.05' stop-opacity='.32'/%3e %3cstop offset='1' stop-opacity='0'/%3e %3c/radialGradient%3e %3c/defs%3e %3c/svg%3e";
IconBase.setDefaultOptions = IconBase.setDefaultOptions || function(options) {
Util.setOptions(this.prototype, options);
return this;
};
export class Icon extends IconBase {
static dropShadowCss = "drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.32))";
static {
Icon.setDefaultOptions({
svg: PinTeardropBorder,
accentColor: "#fff",
color: "#000",
contentColor: "#fff",
contentStyle: {},
origin: "bottom",
rootStyle: {},
scale: 1,
shadow: "cast",
shadowStyle: {},
svgStyle: {},
});
}
constructor(options = {}) {
super(options);
}
initialize(options) {
this.initOptions(options);
this.initCrossOrigin();
this.initSizeAndAnchor(options);
}
initOptions(options) {
for (const [key, value] of Object.entries(options)) {
if (typeof value !== "undefined") {
this.options[key] = value;
}
}
}
initCrossOrigin() {
const opts = this.options;
if (opts.crossOrigin || opts.crossOrigin === "") {
opts.crossOrigin =
opts.crossOrigin === true ? "" : String(opts.crossOrigin);
} else {
opts.crossOrigin = undefined;
}
}
initSizeAndAnchor(options) {
const opts = this.options;
const origin = opts.origin;
const yDivisorMap = {
center: 2,
bottom: 1,
};
opts.iconSize = this.calcIconSize();
const { x, y } = opts.iconSize;
opts.iconAnchor = options.iconAnchor
? new Point(options.iconAnchor)
: new Point(x / 2, y / yDivisorMap[origin]);
if (opts.shadow === "ellipse") {
// 30w 6h
opts.shadowSize = options.shadowSize
? new Point(options.shadowSize)
: new Point(x, (x * 6) / 30);
opts.shadowAnchor = options.shadowAnchor
? new Point(options.shadowAnchor)
: new Point(x / 2, (x * 6) / 30 / 2);
} else if (opts.shadow === "cast") {
// 39w 36h
opts.shadowSize = options.shadowSize
? new Point(options.shadowSize)
: new Point((x * 39) / 30, (x * 36) / 30);
opts.shadowAnchor = options.shadowAnchor
? new Point(options.shadowAnchor)
: new Point(x / 2, (x / 30) * 32);
} else {
// none
opts.shadowSize = options.shadowSize
? new Point(options.shadowSize)
: new Point([0, 0]);
opts.shadowAnchor = options.shadowAnchor
? new Point(options.shadowAnchor)
: new Point([0, 0]);
}
// Set anchors to middle of content wrapper
opts.popupAnchor = options.popupAnchor
? new Point(options.popupAnchor)
: new Point(0, -y + x / 2);
opts.tooltipAnchor = options.tooltipAnchor
? new Point(options.tooltipAnchor)
: new Point(0, -y + x / 2);
}
calcIconSize() {
const opts = this.options;
const scale = Math.max(Math.abs(opts.scale), 0.1);
const origIconWidth = opts.svg?.[1]?.width;
const origIconHeight = opts.svg?.[1]?.height;
const iconWidth = 30 * scale;
const iconHeight = (30 * scale * origIconHeight) / origIconWidth;
const iconSize =
opts.iconSize === "number"
? [opts.iconSize, opts.iconSize]
: opts.iconSize;
if (iconSize) {
return new Point(iconSize);
}
return new Point(iconWidth, iconHeight);
}
createContentWrapper() {
const opts = this.options;
return createElement([
"div",
{
class: ["extra-marker-content", opts.contentWrapperClass],
style: {
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: `${opts.iconSize.x}px`,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
fontSize: `${opts.scale}em`,
fontWeight: "700",
lineHeight: "1",
color: opts.contentColor,
...(opts.contentWrapperStyle ?? {}),
},
},
]);
}
createDot() {
const opts = this.options;
return createElement([
"div",
{
style: {
display: "block",
height: "0.8em",
width: "0.8em",
backgroundColor: opts.accentColor,
borderRadius: "100%",
},
},
]);
}
createImageMarker() {
const opts = this.options;
const url = (Browser.retina && opts.iconRetinaUrl) || opts.iconUrl;
return createElement([
"img",
{
src: url,
crossOrigin: opts.crossOrigin,
style: {
width: `${opts.iconSize.x}px`,
height: `${opts.iconSize.y}px`,
filter: opts.shadow === "drop" ? this.dropShadowCss : "",
...(opts.svgStyle ?? {}),
},
class: ["extra-marker-icon", opts.svgClass],
},
]);
}
createSvgMarker() {
const opts = this.options;
const [svgTag, svgAttrs, svgChildren] = opts.svg;
const svg = createSvgElement([
svgTag,
{
...svgAttrs,
width: `${opts.iconSize.x}px`,
height: `${opts.iconSize.y}px`,
style: {
filter: opts.shadow === "drop" ? Icon.dropShadowCss : "",
...(opts.svgStyle ?? {}),
},
class: ["extra-marker-icon", opts.svgClass],
},
svgChildren,
]);
if (opts.svgFillImageSrc) {
const id = crypto.randomUUID();
svg.firstChild.setAttribute("fill", `url(#${id})`);
svg.prepend(
createSvgElement([
"pattern",
{ id, patternUnits: "userSpaceOnUse", width: "30", height: "30" },
[
[
"image",
{
href: opts.svgFillImageSrc,
width: "30",
height: "30",
crossOrigin: opts.crossOrigin,
},
],
],
]),
);
}
if (svgChildren.length > 1) {
svg.lastChild.setAttribute("fill", opts.accentColor);
}
return svg;
}
createRootElement(children) {
const opts = this.options;
return createElement([
"div",
{
style: {
color: opts.color,
position: "absolute",
width: `${opts.iconSize.x}px`,
height: `${opts.iconSize.y}px`,
marginLeft: `${-opts.iconAnchor.x}px`,
marginTop: `${-opts.iconAnchor.y}px`,
fontSize: "12px",
...(opts.rootStyle ?? {}),
},
class: ["extra-marker", opts.className, opts.rootClass],
},
children,
]);
}
createIcon() {
const opts = this.options;
const marker =
opts.iconUrl || opts.iconRetinaUrl
? this.createImageMarker()
: this.createSvgMarker();
const contentWrapper = this.createContentWrapper();
// InnerHtml > function > content > svgFillImageSrc > empty dot
if (opts.contentHtml) {
contentWrapper.innerHTML = opts.contentHtml;
} else if (typeof opts.content === "function") {
contentWrapper.append(opts.content(opts));
} else if (
(opts.content !== null) &
(typeof opts.content !== "undefined")
) {
contentWrapper.append(opts.content);
} else if (!opts.svgFillImageSrc) {
contentWrapper.append(this.createDot());
}
return this.createRootElement([marker, contentWrapper]);
}
createShadowImg() {
const opts = this.options;
const url = (Browser.retina && opts.shadowRetinaUrl) || opts.shadowUrl;
const svgUri = opts.shadow === "ellipse" ? shadowEllipse : shadowCast;
return createElement([
"img",
{
src: url ?? svgUri,
class: ["extra-marker-shadow", opts.className, opts.shadowClass],
style: {
position: "absolute",
width: `${opts.shadowSize.x}px`,
height: `${opts.shadowSize.y}px`,
marginLeft: `${-opts.shadowAnchor.x}px`,
marginTop: `${-opts.shadowAnchor.y}px`,
...(opts.shadowStyle ?? {}),
},
crossOrigin: url ? opts.crossOrigin : undefined,
},
]);
}
createShadow() {
const opts = this.options;
if ((opts.shadow === "none") | (opts.shadow === "drop")) return;
return this.createShadowImg();
}
}