UNPKG

leaflet-extra-markers

Version:
319 lines (283 loc) 9.82 kB
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(); } }