|
import * as L from "https://unpkg.com/[email protected]/dist/leaflet-src.esm.js" |
|
|
|
// workaround for using leaflet in shadowdom |
|
L.Icon.Default.imagePath = "https://unpkg.com/[email protected]/dist/images/" |
|
|
|
customElements.define("positions-map", |
|
class extends HTMLElement { |
|
map = null |
|
markers = [] |
|
gridlines = [] |
|
|
|
constructor() { |
|
super() |
|
this.attachShadow({ mode: "open" }).innerHTML = ` |
|
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"/> |
|
<style> |
|
#map { |
|
width: 100%; |
|
height: 100%; |
|
} |
|
</style> |
|
<div id="map"></div> |
|
` |
|
this._container = this.shadowRoot.getElementById("map") |
|
} |
|
|
|
connectedCallback() { |
|
if (this.map) return |
|
|
|
this.map = L.map(this._container, { |
|
center: [35, 135], |
|
zoom: 7, |
|
}) |
|
|
|
L.tileLayer("https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png", { |
|
attribution: ` |
|
<a href="https://maps.gsi.go.jp/development/ichiran.html" target="_blank">地理院タイル</a> |
|
`, |
|
}).addTo(this.map) |
|
|
|
this.map.on("load moveend", () => { |
|
this.render() |
|
}) |
|
} |
|
|
|
init(config) { |
|
const defaults = { |
|
positions: [], |
|
rows: 10, |
|
columns: 10, |
|
threshold: 5, |
|
createMarkerIcon: null, |
|
createGroupIcon: null, |
|
grid: false, |
|
} |
|
this.config = { ...defaults, ...config } |
|
this.render() |
|
} |
|
|
|
render() { |
|
this.clearMarkers() |
|
this.clearGrid() |
|
if (!this.config) return |
|
|
|
const { rows, columns, w, s, e, n, cells } = this.calc() |
|
|
|
for (const [cell_id, positions] of Object.entries(cells)) { |
|
const [lao, lno] = cell_id.split(",").map(e => +e) |
|
if (positions.length <= this.config.threshold) { |
|
for (const { lat, lng, title } of positions) { |
|
const icon = this.config.createMarkerIcon |
|
? L.icon(this.config.createMarkerIcon(position)) |
|
: null |
|
this.addMarker({ |
|
latlng: { lat, lng }, |
|
icon, |
|
title, |
|
}) |
|
} |
|
} else { |
|
const dr = (n - s) / rows |
|
const dc = (e - w) / columns |
|
const cell = { |
|
w: w + dc * lno, |
|
s: s + dr * lao, |
|
e: w + dc * (lno + 1), |
|
n: s + dr * (lao + 1), |
|
} |
|
const center = { lat: (cell.s + cell.n) / 2, lng: (cell.w + cell.e) / 2 } |
|
const icon = this.config.createGroupIcon |
|
? L.icon(this.config.createGroupIcon(position)) |
|
: L.icon({ |
|
iconUrl: this.createSvgIcon(positions.length), |
|
iconSize: [50, 50], |
|
iconAnchor: [25, 25], |
|
}) |
|
this.addMarker({ |
|
latlng: center, |
|
icon, |
|
onclick: () => { |
|
this.map.fitBounds([[cell.s, cell.w], [cell.n, cell.e]]) |
|
}, |
|
}) |
|
} |
|
} |
|
|
|
if (this.config.grid) { |
|
for (let i = 0; i <= rows; i++) { |
|
const d = (n - s) / rows |
|
const lat = s + d * i |
|
this.addLine([lat, w], [lat, e]) |
|
} |
|
for (let i = 0; i <= columns; i++) { |
|
const d = (e - w) / columns |
|
const lng = w + d * i |
|
this.addLine([s, lng], [n, lng]) |
|
} |
|
} |
|
} |
|
|
|
addMarker({ latlng, icon, title, onclick } = {}) { |
|
const marker = icon ? L.marker(latlng, { icon }) : L.marker(latlng) |
|
if (title) { |
|
marker.bindTooltip(title) |
|
} |
|
if (onclick) { |
|
marker.on("click", onclick) |
|
} |
|
marker.addTo(this.map) |
|
this.markers.push(marker) |
|
} |
|
|
|
clearMarkers() { |
|
let m |
|
while (m = this.markers.shift()) { |
|
m.remove() |
|
} |
|
} |
|
|
|
addLine(begin, end) { |
|
const line = L.polyline( |
|
[begin, end], |
|
{ |
|
color: "#746858", |
|
interactive: false, |
|
weight: 1, |
|
fill: false, |
|
} |
|
).addTo(this.map) |
|
this.gridlines.push(line) |
|
} |
|
|
|
clearGrid() { |
|
let l |
|
while (l = this.gridlines.shift()) { |
|
l.remove() |
|
} |
|
} |
|
|
|
createSvgIcon(num) { |
|
return "data:image/svg+xml," + |
|
encodeURIComponent(` |
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 500 500" xml:space="preserve"> |
|
<circle xmlns="http://www.w3.org/2000/svg" cx="250" cy="250" r="248" fill="#978874"/> |
|
<text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white" font-size="150">${num}</text> |
|
</svg> |
|
`) |
|
} |
|
|
|
calc() { |
|
const { rows, columns } = this.config |
|
const [w, s, e, n] = this.map |
|
.getBounds() |
|
.toBBoxString() |
|
.split(",") |
|
.map(e => +e) |
|
|
|
const cells = {} |
|
for (const position of this.config.positions) { |
|
if (position.lat < s || position.lat > n || position.lng < w || position.lng > e) continue |
|
|
|
let lao = ~~((position.lat - s) / ((n - s) / rows)) |
|
if (lao === rows) lao = rows - 1 |
|
let lno = ~~((position.lng - w) / ((e - w) / columns)) |
|
if (lno === columns) lno = columns - 1 |
|
|
|
const cell_id = [lao, lno].join(",") |
|
if (cells[cell_id]) { |
|
cells[cell_id].push(position) |
|
} else { |
|
cells[cell_id] = [position] |
|
} |
|
} |
|
|
|
return { rows, columns, w, s, e, n, cells } |
|
} |
|
} |
|
) |