Files
tabler/preview/pages/maps.astro
T

424 lines
19 KiB
Plaintext

---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Map from '@components/demo/Map.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import maps from '@data/maps.json'
import CardTitle from '@ui/CardTitle.astro'
import CardSubtitle from '@ui/CardSubtitle.astro'
import DocsLink from '@ui/DocsLink.astro'
import Avatar from '@ui/Avatar.astro'
import Icon from '@ui/Icon.astro'
import { staticPath } from '@shared/lib/assets'
import CaptureScript from '@shared/components/CaptureScript.astro'
type MapData = { title?: string; card?: boolean }
const mapEntries = Object.entries(maps as Record<string, MapData>)
const route = [
[13.3695, 52.5251],
[13.3777, 52.5208],
[13.388, 52.5186],
[13.3989, 52.5176],
[13.405, 52.5192],
[13.4132, 52.5219],
[13.4215, 52.5165],
[13.43, 52.5105],
[13.4397, 52.505],
]
const area = [
[13.37, 52.53],
[13.42, 52.535],
[13.445, 52.52],
[13.43, 52.505],
[13.385, 52.503],
[13.365, 52.515],
[13.37, 52.53],
]
const cities = [
{ name: 'Berlin', lng: 13.405, lat: 52.52, users: 820 },
{ name: 'London', lng: -0.1276, lat: 51.5072, users: 1240 },
{ name: 'New York', lng: -74.006, lat: 40.7128, users: 1580 },
{ name: 'Tokyo', lng: 139.6917, lat: 35.6895, users: 960 },
{ name: 'São Paulo', lng: -46.6333, lat: -23.5505, users: 540 },
{ name: 'Sydney', lng: 151.2093, lat: -33.8688, users: 310 },
{ name: 'Lagos', lng: 3.3792, lat: 6.5244, users: 420 },
{ name: 'Mumbai', lng: 72.8777, lat: 19.076, users: 1100 },
]
// Stores scattered around a few European cities; the offsets are fixed, so every build draws the same map
const storeCities: [number, number][] = [
[13.405, 52.52],
[2.3522, 48.8566],
[-0.1276, 51.5072],
[12.4964, 41.9028],
[-3.7038, 40.4168],
[21.0122, 52.2297],
[16.3738, 48.2082],
[4.9041, 52.3676],
]
const stores = storeCities.flatMap(([lng, lat], city) => Array.from({ length: 5 + (city % 4) }, (_, index) => [lng + Math.sin(city * 7 + index * 3) * 0.6, lat + Math.cos(city * 5 + index * 2) * 0.4]))
const hub = { name: 'London', lng: -0.1276, lat: 51.5072 }
const destinations = [
{ name: 'Tokyo', lng: 139.6917, lat: 35.6895 },
{ name: 'Mumbai', lng: 72.8777, lat: 19.076 },
{ name: 'Singapore', lng: 103.8198, lat: 1.3521 },
{ name: 'Nairobi', lng: 36.8219, lat: -1.2921 },
]
const places = [
{ name: 'Museum Island', text: 'Five museums on the Spree, open daily.', photo: 'a-visit-to-the-bookstore.jpg', lat: 52.5169, lng: 13.4019, open: true },
{ name: 'Bellevue Palace', text: 'Residence by the Tiergarten park.', photo: 'book-on-the-grass.jpg', lat: 52.5176, lng: 13.3531, open: false },
{ name: 'Weissensee', text: 'A lake with a beach in the north-east.', photo: 'brainstorming-session-with-creative-designers.jpg', lat: 52.5546, lng: 13.4633, open: false },
]
---
<DefaultLayout title="Maps" pageMenu="plugins.maps" pageLibs={['maplibre-gl']} description="Interactive MapLibre maps — plain, styled, with custom markers and popups.">
<DocsLink slot="page-header-actions" path="/ui/components/map" />
<div class="row row-cards">
{
mapEntries.map(([mapId, data]) =>
data.card ? (
<div class="col-lg-12">
<Card>
<Map mapId={mapId} ratio="21x9" />
</Card>
</div>
) : (
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>{data.title}</CardTitle>
<Map mapId={mapId} />
</CardBody>
</Card>
</div>
),
)
}
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Marker styles</CardTitle>
<CardSubtitle>Draw markers in HTML with <code>.map-marker</code>: a dot, a circle with an icon or a count, a pin, or an avatar. Add <code>.map-marker-label</code> for a short text.</CardSubtitle>
<Map mapId="marker-styles" title="Marker styles" center={[52.522, 13.405]} zoom={11} ratio="4x3">
<span class="map-marker map-marker-dot" data-bs-lng="13.345" data-bs-lat="52.54"></span>
<span class="map-marker map-marker-dot map-marker-red" data-bs-lng="13.39" data-bs-lat="52.545"></span>
<span class="map-marker" data-bs-lng="13.445" data-bs-lat="52.538"><Icon name="building-store" /><span class="map-marker-label">Store</span></span>
<span class="map-marker map-marker-green" data-bs-lng="13.35" data-bs-lat="52.512">12</span>
<span class="map-marker map-marker-pin" data-bs-lng="13.405" data-bs-lat="52.52" data-bs-anchor="bottom"><Icon name="home" /></span>
<span class="map-marker map-marker-pin map-marker-red" data-bs-lng="13.47" data-bs-lat="52.518" data-bs-anchor="bottom"><Icon name="star" /></span>
<span class="map-marker" data-bs-lng="13.43" data-bs-lat="52.503"><Avatar size="sm" src={staticPath('avatars/000m.jpg')} /></span>
</Map>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Popup card</CardTitle>
<CardSubtitle>Put the popup content in a <code>&lt;template&gt;</code> inside the marker. <code>.map-popup-card</code> makes room for an image.</CardSubtitle>
<Map mapId="popup-card" title="Popup card" center={[52.546, 13.4]} zoom={11} ratio="4x3">
{
places.map((place) => (
<span class="map-marker map-marker-pin" data-bs-lng={place.lng} data-bs-lat={place.lat} data-bs-anchor="bottom" data-bs-popup-open={place.open ? 'true' : undefined}>
<Icon name="map-pin" />
<template class="map-popup-card" data-bs-close-button="true">
<div class="img-responsive img-responsive-21x9" style={`background-image: url(${staticPath(`photos/${place.photo}`)})`} />
<div class="p-3">
<div class="fw-medium">{place.name}</div>
<div class="text-secondary mb-3">{place.text}</div>
<a href="#" class="btn btn-primary btn-sm">
View details
</a>
</div>
</template>
</span>
))
}
</Map>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Route</CardTitle>
<CardSubtitle>Draw a path with a GeoJSON line layer, and mark its ends with markers.</CardSubtitle>
<Map mapId="route" title="Route" center={[52.513, 13.4046]} zoom={11.6}>
<span class="map-marker map-marker-dot" data-bs-lng="13.3695" data-bs-lat="52.5251"></span>
<span class="map-marker map-marker-pin" data-bs-lng="13.4397" data-bs-lat="52.505" data-bs-anchor="bottom"><Icon name="map-pin" /></span>
</Map>
<CaptureScript>
<script is:inline define:vars={{ route }}>
document.addEventListener('DOMContentLoaded', () => {
const map = tabler.MapView.getInstance('#map-route').map
// Layers are part of the style, so add them again each time the style loads
map.on('style.load', () => {
map.addSource('route', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates: route } } })
map.addLayer({
id: 'route',
type: 'line',
source: 'route',
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': tabler.MapView.color('--tblr-primary'), 'line-width': 3, 'line-opacity': 0.8 },
})
})
})
</script>
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Area</CardTitle>
<CardSubtitle>Show a zone with a GeoJSON polygon: a fill layer and a line layer for its outline.</CardSubtitle>
<Map mapId="area" title="Area" center={[52.519, 13.405]} zoom={11} />
<CaptureScript>
<script is:inline define:vars={{ area }}>
document.addEventListener('DOMContentLoaded', () => {
const map = tabler.MapView.getInstance('#map-area').map
map.on('style.load', () => {
const color = tabler.MapView.color('--tblr-primary')
map.addSource('area', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'Polygon', coordinates: [area] } } })
map.addLayer({ id: 'area-fill', type: 'fill', source: 'area', paint: { 'fill-color': color, 'fill-opacity': 0.16 } })
map.addLayer({ id: 'area-outline', type: 'line', source: 'area', paint: { 'line-color': color, 'line-width': 2 } })
})
})
</script>
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Clusters</CardTitle>
<CardSubtitle>Group many points into clusters that split as you zoom in. Click a cluster to zoom to it.</CardSubtitle>
<Map mapId="clusters" title="Clusters" center={[48.5, 9]} zoom={3} controls />
<CaptureScript>
<script is:inline define:vars={{ stores }}>
document.addEventListener('DOMContentLoaded', () => {
const map = tabler.MapView.getInstance('#map-clusters').map
map.on('style.load', () => {
const color = tabler.MapView.color('--tblr-primary')
const border = tabler.MapView.color('--tblr-bg-surface')
map.addSource('stores', {
type: 'geojson',
data: { type: 'FeatureCollection', features: stores.map((coordinates) => ({ type: 'Feature', geometry: { type: 'Point', coordinates } })) },
cluster: true,
clusterRadius: 40,
})
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'stores',
filter: ['has', 'point_count'],
paint: { 'circle-color': color, 'circle-radius': ['step', ['get', 'point_count'], 14, 10, 18, 25, 24], 'circle-stroke-width': 2, 'circle-stroke-color': border },
})
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'stores',
filter: ['has', 'point_count'],
layout: { 'text-field': ['get', 'point_count_abbreviated'], 'text-font': ['Noto Sans Regular'], 'text-size': 12 },
paint: { 'text-color': tabler.MapView.color('--tblr-primary-fg') },
})
map.addLayer({
id: 'store',
type: 'circle',
source: 'stores',
filter: ['!', ['has', 'point_count']],
paint: { 'circle-color': color, 'circle-radius': 6, 'circle-stroke-width': 2, 'circle-stroke-color': border },
})
})
map.on('click', 'clusters', (event) => {
const cluster = event.features[0]
map
.getSource('stores')
.getClusterExpansionZoom(cluster.properties.cluster_id)
.then((zoom) => map.easeTo({ center: cluster.geometry.coordinates, zoom }))
})
})
</script>
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>City switcher</CardTitle>
<CardSubtitle>Put buttons over the map and move it to a place with <code>flyTo</code>.</CardSubtitle>
<Map mapId="cities" title="City switcher" center={[52.52, 13.405]} zoom={10}>
<div slot="overlay" class="btn-list position-absolute top-0 start-0 w-auto h-auto m-2" role="group" aria-label="City" id="map-cities-switcher">
{
cities.slice(0, 4).map((city, index) => (
<button type="button" class:list={['btn btn-sm btn-pill', index === 0 && 'btn-primary']} aria-pressed={index === 0 ? 'true' : 'false'} data-lng={city.lng} data-lat={city.lat}>
{city.name}
</button>
))
}
</div>
</Map>
<CaptureScript>
<script is:inline>
document.addEventListener('DOMContentLoaded', () => {
const map = tabler.MapView.getInstance('#map-cities').map
const buttons = document.querySelectorAll('#map-cities-switcher .btn')
buttons.forEach((button) => {
button.addEventListener('click', () => {
buttons.forEach((item) => {
item.classList.toggle('btn-primary', item === button)
item.setAttribute('aria-pressed', item === button)
})
map.flyTo({ center: [Number(button.dataset.lng), Number(button.dataset.lat)], zoom: 10 })
})
})
})
</script>
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-12">
<Card>
<CardBody>
<CardTitle>Bubble map</CardTitle>
<CardSubtitle>Size circles by a value, for example users per city. Hover a circle to see the number.</CardSubtitle>
<Map mapId="bubbles" title="Bubble map" center={[20, 20]} zoom={1} ratio="21x9" />
<CaptureScript>
<script is:inline define:vars={{ cities }}>
document.addEventListener('DOMContentLoaded', () => {
const map = tabler.MapView.getInstance('#map-bubbles').map
const tooltip = new maplibregl.Popup({ closeButton: false, offset: 12, className: 'map-tooltip' })
map.on('style.load', () => {
const color = tabler.MapView.color('--tblr-primary')
map.addSource('cities', {
type: 'geojson',
data: { type: 'FeatureCollection', features: cities.map((city) => ({ type: 'Feature', properties: city, geometry: { type: 'Point', coordinates: [city.lng, city.lat] } })) },
})
map.addLayer({
id: 'cities',
type: 'circle',
source: 'cities',
paint: { 'circle-color': color, 'circle-opacity': 0.6, 'circle-radius': ['interpolate', ['linear'], ['get', 'users'], 0, 4, 1600, 24] },
})
})
map.on('mousemove', 'cities', (event) => {
const city = event.features[0].properties
map.getCanvas().style.cursor = 'pointer'
tooltip.setLngLat([city.lng, city.lat]).setText(`${city.name}: ${city.users} users`).addTo(map)
})
map.on('mouseleave', 'cities', () => {
map.getCanvas().style.cursor = ''
tooltip.remove()
})
})
</script>
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Globe</CardTitle>
<CardSubtitle>Show the map as a 3D globe with <code>data-bs-globe="true"</code>, and connect places with arcs.</CardSubtitle>
<Map mapId="globe" title="Globe" center={[32, 62]} zoom={1.3} ratio="4x3" globe>
<span class="map-marker map-marker-dot" data-bs-lng={hub.lng} data-bs-lat={hub.lat}>
<span class="map-marker-label">{hub.name}</span>
</span>
{
destinations.map((place) => (
<span class="map-marker map-marker-dot" style="--tblr-map-marker-size: 0.625rem" data-bs-lng={place.lng} data-bs-lat={place.lat}>
<span class="map-marker-label">{place.name}</span>
</span>
))
}
</Map>
<CaptureScript>
<script is:inline define:vars={{ hub, destinations }}>
document.addEventListener('DOMContentLoaded', () => {
const map = tabler.MapView.getInstance('#map-globe').map
// Points along the shortest path between two places on the globe
const arc = (from, to, steps = 64) => {
const [lng1, lat1, lng2, lat2] = [from.lng, from.lat, to.lng, to.lat].map((degrees) => (degrees * Math.PI) / 180)
const angle = 2 * Math.asin(Math.sqrt(Math.sin((lat2 - lat1) / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin((lng2 - lng1) / 2) ** 2))
return Array.from({ length: steps + 1 }, (_, step) => {
const start = Math.sin((1 - step / steps) * angle) / Math.sin(angle)
const end = Math.sin((step / steps) * angle) / Math.sin(angle)
const x = start * Math.cos(lat1) * Math.cos(lng1) + end * Math.cos(lat2) * Math.cos(lng2)
const y = start * Math.cos(lat1) * Math.sin(lng1) + end * Math.cos(lat2) * Math.sin(lng2)
const z = start * Math.sin(lat1) + end * Math.sin(lat2)
return [(Math.atan2(y, x) * 180) / Math.PI, (Math.atan2(z, Math.hypot(x, y)) * 180) / Math.PI]
})
}
map.on('style.load', () => {
// Hide the place names of the map style, so only the marker labels are left
map
.getStyle()
.layers.filter((layer) => layer.type === 'symbol')
.forEach((layer) => map.setLayoutProperty(layer.id, 'visibility', 'none'))
map.addSource('arcs', {
type: 'geojson',
data: { type: 'FeatureCollection', features: destinations.map((place) => ({ type: 'Feature', geometry: { type: 'LineString', coordinates: arc(hub, place) } })) },
})
map.addLayer({
id: 'arcs',
type: 'line',
source: 'arcs',
paint: { 'line-color': tabler.MapView.color('--tblr-primary'), 'line-width': 1.5, 'line-dasharray': [2, 2] },
})
})
})
</script>
</CaptureScript>
</CardBody>
</Card>
</div>
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>Draggable marker</CardTitle>
<CardSubtitle>Add <code>data-bs-draggable="true"</code> to let people move a marker, for example to pick an address.</CardSubtitle>
<Map mapId="draggable" title="Draggable marker" center={[52.52, 13.405]} zoom={12} ratio="4x3">
<span class="map-marker map-marker-pin" data-bs-lng="13.405" data-bs-lat="52.52" data-bs-anchor="bottom" data-bs-draggable="true"><Icon name="map-pin" /></span>
</Map>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout>