mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
424 lines
19 KiB
Plaintext
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><template></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>
|