Add leaflet map with clustered photos
This commit is contained in:
parent
a4bf1bb37a
commit
76290df734
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -19,3 +19,6 @@ pnpm-debug.log*
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# binary blobs
|
||||||
|
public/**/*.jpg
|
||||||
|
|
|
@ -11,9 +11,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"astro": "^2.2.0",
|
"astro": "^2.2.0",
|
||||||
"dayjs": "^1.11.7"
|
"dayjs": "^1.11.7",
|
||||||
|
"leaflet": "^1.9.3",
|
||||||
|
"leaflet.markercluster": "^1.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/leaflet": "^1.9.3",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.1",
|
||||||
"sass": "^1.61.0"
|
"sass": "^1.61.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,8 +7,20 @@ dependencies:
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.7
|
specifier: ^1.11.7
|
||||||
version: 1.11.7
|
version: 1.11.7
|
||||||
|
leaflet:
|
||||||
|
specifier: ^1.9.3
|
||||||
|
version: 1.9.3
|
||||||
|
leaflet.markercluster:
|
||||||
|
specifier: ^1.5.3
|
||||||
|
version: 1.5.3(leaflet@1.9.3)
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/leaflet':
|
||||||
|
specifier: ^1.9.3
|
||||||
|
version: 1.9.3
|
||||||
|
'@types/leaflet.markercluster':
|
||||||
|
specifier: ^1.5.1
|
||||||
|
version: 1.5.1
|
||||||
sass:
|
sass:
|
||||||
specifier: ^1.61.0
|
specifier: ^1.61.0
|
||||||
version: 1.61.0
|
version: 1.61.0
|
||||||
|
@ -660,6 +672,10 @@ packages:
|
||||||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/geojson@7946.0.10:
|
||||||
|
resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/hast@2.3.4:
|
/@types/hast@2.3.4:
|
||||||
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
|
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -670,6 +686,18 @@ packages:
|
||||||
resolution: {integrity: sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==}
|
resolution: {integrity: sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/leaflet.markercluster@1.5.1:
|
||||||
|
resolution: {integrity: sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/leaflet': 1.9.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/leaflet@1.9.3:
|
||||||
|
resolution: {integrity: sha512-Caa1lYOgKVqDkDZVWkto2Z5JtVo09spEaUt2S69LiugbBpoqQu92HYFMGUbYezZbnBkyOxMNPXHSgRrRY5UyIA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': 7946.0.10
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/mdast@3.0.11:
|
/@types/mdast@3.0.11:
|
||||||
resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==}
|
resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1588,6 +1616,18 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/leaflet.markercluster@1.5.3(leaflet@1.9.3):
|
||||||
|
resolution: {integrity: sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==}
|
||||||
|
peerDependencies:
|
||||||
|
leaflet: ^1.3.1
|
||||||
|
dependencies:
|
||||||
|
leaflet: 1.9.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/leaflet@1.9.3:
|
||||||
|
resolution: {integrity: sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/load-yaml-file@0.2.0:
|
/load-yaml-file@0.2.0:
|
||||||
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
|
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
6442
public/static/map/data.json
Normal file
6442
public/static/map/data.json
Normal file
File diff suppressed because it is too large
Load diff
94
src/assets/leaflet-photo/index.ts
Normal file
94
src/assets/leaflet-photo/index.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet.markercluster'
|
||||||
|
|
||||||
|
|
||||||
|
interface Photo extends L.LatLngLiteral {
|
||||||
|
thumbnail: string;
|
||||||
|
photoUrl: string;
|
||||||
|
caption: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
L.Photo = L.FeatureGroup.extend({
|
||||||
|
options: {
|
||||||
|
icon: {
|
||||||
|
iconSize: [40, 40] as L.PointTuple
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function (photos: Photo[], options: any) {
|
||||||
|
L.setOptions(this, options);
|
||||||
|
// @ts-ignore
|
||||||
|
L.FeatureGroup.prototype.initialize.call(this, photos);
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayers: function (photos: Photo[]) {
|
||||||
|
for (const photo of photos)
|
||||||
|
this.addLayer(photo);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayer: function (photo: Photo) {
|
||||||
|
L.FeatureGroup.prototype.addLayer.call(this, this.createMarker(photo));
|
||||||
|
},
|
||||||
|
|
||||||
|
createMarker: function (photo: Photo) {
|
||||||
|
const marker = L.marker(photo, {
|
||||||
|
icon: L.divIcon(L.extend({
|
||||||
|
html: `<div style="background-image: url(${photo.thumbnail});"></div>`,
|
||||||
|
className: 'leaflet-marker-photo'
|
||||||
|
}, photo, this.options.icon)),
|
||||||
|
title: photo.caption || ''
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
marker.photo = photo;
|
||||||
|
return marker;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.photo = function (photos, options) {
|
||||||
|
return new L.Photo(photos, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (L.MarkerClusterGroup) {
|
||||||
|
L.Photo.Cluster = L.MarkerClusterGroup.extend({
|
||||||
|
options: {
|
||||||
|
featureGroup: L.photo,
|
||||||
|
maxClusterRadius: 100,
|
||||||
|
showCoverageOnHover: false,
|
||||||
|
icon: { iconSize: [40, 40] as L.PointTuple },
|
||||||
|
|
||||||
|
iconCreateFunction: function(cluster: any) {
|
||||||
|
const markers = cluster.getAllChildMarkers();
|
||||||
|
return new L.DivIcon(L.extend({
|
||||||
|
html: `<div style="background-image: url(${markers[0].photo.thumbnail});"></div><b>${markers.length}</b>`,
|
||||||
|
className: 'leaflet-marker-photo',
|
||||||
|
}, this.icon));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function (options: any) {
|
||||||
|
options = L.Util.setOptions(this, options);
|
||||||
|
// @ts-ignore
|
||||||
|
L.MarkerClusterGroup.prototype.initialize.call(this);
|
||||||
|
this._photos = options.featureGroup(null, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
add: function (photos: Photo) {
|
||||||
|
this.addLayer(this._photos.addLayers(photos));
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function () {
|
||||||
|
this._photos.clearLayers();
|
||||||
|
this.clearLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
L.photo.cluster = function (options: any) {
|
||||||
|
return new L.Photo.Cluster!(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
27
src/assets/leaflet-photo/styles.scss
Normal file
27
src/assets/leaflet-photo/styles.scss
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.leaflet-marker-photo {
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 3px 3px 10px #888;
|
||||||
|
|
||||||
|
div {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
position: absolute;
|
||||||
|
top: -7px;
|
||||||
|
right: -11px;
|
||||||
|
color: #555;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 12px;
|
||||||
|
min-width: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 3px;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
}
|
12
src/assets/leaflet-photo/types.d.ts
vendored
Normal file
12
src/assets/leaflet-photo/types.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import 'leaflet';
|
||||||
|
|
||||||
|
declare module 'leaflet' {
|
||||||
|
class Photo extends L.FeatureGroup {
|
||||||
|
static Cluster?: { new(...args: any[]): any } & L.Class;
|
||||||
|
}
|
||||||
|
|
||||||
|
let photo: {
|
||||||
|
(photos: Photo[], options: any): Photo;
|
||||||
|
cluster?: (options?: any) => any;
|
||||||
|
};
|
||||||
|
}
|
49
src/pages/map.astro
Normal file
49
src/pages/map.astro
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
import Base from "../layouts/Base.astro";
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||||
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||||
|
import '../assets/leaflet-photo/styles.scss';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base>
|
||||||
|
<div id="map" style="height: 100%; width: 100%"></div>
|
||||||
|
<script>
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet.markercluster';
|
||||||
|
import '../assets/leaflet-photo';
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="popup">
|
||||||
|
<a href="{photo}">
|
||||||
|
<img width="{width}" height="{height}" src="{photo}" alt="" />
|
||||||
|
<div class="meta">
|
||||||
|
<span class="date">{date}</span><span class="caption">{caption}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const map = L.map('map').setView([51.85, 16.57], 13);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const photoLayer = L.photo.cluster!().on('click', function(evt: any) {
|
||||||
|
evt.layer.bindPopup(L.Util.template(template, evt.layer.photo)).openPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const data = await fetch('/static/map/data.json');
|
||||||
|
if (!data.ok) return;
|
||||||
|
|
||||||
|
return await data.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add photos to the map
|
||||||
|
loadData().then(data => photoLayer.add(data).addTo(map));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div slot="footer"></div>
|
||||||
|
</Base>
|
Loading…
Reference in a new issue