Skip to content

Instantly share code, notes, and snippets.

@nexpr
Last active June 4, 2021 11:15
Show Gist options
  • Save nexpr/4fb41c54afe25bb66ecd9764b5e1462c to your computer and use it in GitHub Desktop.
Save nexpr/4fb41c54afe25bb66ecd9764b5e1462c to your computer and use it in GitHub Desktop.
leaflet map に多くの位置を表示する を CustomElement 化したもの

leaflet map に多くの位置を表示する を CustomElement 化したもの

関連: https://gist.github.com/nexpr/ab62497806bd422185bd94c1675afd48

前回のはモジュール化してなくて使いまわしづらかったので Custom Element 化した

example.html は前と同じ郵便局データの表示

positions-map

leaflet map を使って M x N のグリッド分割して表示するのは固定

設定は init メソッドの呼び出し

elem.init({
	positions: [],
	rows: 10,
	columns: 10,
	threshold: 5,
	grid: false,
})
  • poisitions
    • { lat, lng, title } の配列
    • デフォルト: []
  • rows
    • 行数
    • デフォルト: 10
  • columns
    • 列数
    • デフォルト: 10
  • threshold
    • セル内の数がこれを超えるとまとめて個数の表示にする
    • デフォルト: 5
  • grid
    • グリッドラインの表示/非表示
    • デフォルト: false
<!DOCTYPE html>
<meta charset="UTF-8">
<title>poffice map</title>
<style>
positions-map {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
<positions-map id="pm"></positions-map>
<script type="module">
import "./positions-map.js"
const res = await fetch("https://gistcdn.githack.com/nexpr/ab62497806bd422185bd94c1675afd48/raw/e5ad12d079f51a4af1e0a7ccaab96a5ebf6d075e/poffice.json")
const data = await res.json()
const positions = data.map(d => ({ lat: d.lat, lng: d.lng, title: d.name }))
pm.init({
positions,
grid: true,
})
</script>
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 }
}
}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment