Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo/DemoMapBasic.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function MapInner () {
mapStyle: MAP_STYLE,
center: [-2.9631008,54.432306],
zoom: 15,
containerHeight: '500px'
containerHeight: '516px'
})
})
}, [])
Expand Down
171 changes: 171 additions & 0 deletions demo/DemoMapDraw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { useEffect, useRef } from 'react'
import BrowserOnly from '@docusaurus/BrowserOnly'

const MAP_STYLE = {
id: 'outdoor',
url: 'https://labs.os.uk/tiles/styles/open-zoomstack-outdoor/style.json',
attribution: `Contains OS data © Crown copyright and database rights ${new Date().getFullYear()}`,
backgroundColor: '#f5f5f0'
}

const DRAW_LAYERS = ['fill-inactive.cold', 'stroke-inactive.cold']
const ICON_POLYGON = '<path d="M19.5 7v10M4.5 7v10M7 19.5h10M7 4.5h10"/><path d="M22 18v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm0-15v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zM7 18v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zM7 3v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1z"/>'
const ICON_LINE = '<path d="M5.706 16.294L16.294 5.706"/><path d="M21 2v3c0 .549-.451 1-1 1h-3c-.549 0-1-.451-1-1V2c0-.549.451-1 1-1h3c.549 0 1 .451 1 1zM6 17v3c0 .549-.451 1-1 1H2c-.549 0-1-.451-1-1v-3c0-.549.451-1 1-1h3c.549 0 1 .451 1 1z"/>'
const ICON_EDIT = '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'
const ICON_DELETE = '<path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'

function MapInner () {
const initialised = useRef(false)

useEffect(() => {
if (initialised.current) {
return
}
initialised.current = true

Promise.all([
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/interact/src/index.js'),
import('../plugins/beta/draw-ml/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
{ default: createInteractPlugin },
{ default: createDrawPlugin }
]) => {
const interactPlugin = createInteractPlugin({
layers: [
{ layerId: 'fill-inactive.cold', idProperty: 'id' },
{ layerId: 'stroke-inactive.cold', idProperty: 'id' }
],
interactionModes: ['selectFeature'],
multiSelect: true,
deselectOnClickOutside: true
})

const drawPlugin = createDrawPlugin({
snapLayers: ['buildings 3D']
})

const interactiveMap = new InteractiveMap('demo-map-draw', {
behaviour: 'hybrid',
mapProvider: maplibreProvider(),
mapStyle: MAP_STYLE,
center: [-0.1276, 51.5074],
zoom: 12,
containerHeight: '516px',
plugins: [interactPlugin, drawPlugin],
hasExitButton: true
})

let selectedFeatureIds = []

interactiveMap.on('map:ready', () => {
interactPlugin.enable()

interactiveMap.addButton('drawTools', {
label: 'Draw tools',
mobile: { slot: 'bottom-right' },
tablet: { slot: 'top-middle' },
desktop: { slot: 'top-middle' },
menuItems: [
{
id: 'drawPolygon',
label: 'Draw polygon',
iconSvgContent: ICON_POLYGON,
onClick: () => {
interactiveMap.toggleButtonState('drawTools', 'hidden', true)
drawPlugin.newPolygon(crypto.randomUUID(), {
stroke: '#e6c700',
fill: 'rgba(255, 221, 0, 0.1)'
})
}
},
{
id: 'drawLine',
label: 'Draw line',
iconSvgContent: ICON_LINE,
onClick: () => {
interactiveMap.toggleButtonState('drawTools', 'hidden', true)
drawPlugin.newLine(crypto.randomUUID(), {
stroke: '#d4351c',
strokeWidth: 4
})
}
},
{
id: 'editFeature',
label: 'Edit feature',
iconSvgContent: ICON_EDIT,
isDisabled: true,
onClick: () => {
if (!drawPlugin.editFeature(selectedFeatureIds[0])) return
interactiveMap.toggleButtonState('drawTools', 'hidden', true)
interactPlugin.disable()
}
},
{
id: 'deleteFeature',
label: 'Delete feature',
iconSvgContent: ICON_DELETE,
isDisabled: true,
onClick: () => {
drawPlugin.deleteFeature(selectedFeatureIds)
interactPlugin.clear()
interactiveMap.toggleButtonState('drawTools', 'hidden', false)
interactiveMap.toggleButtonState('drawPolygon', 'disabled', false)
interactiveMap.toggleButtonState('drawLine', 'disabled', false)
interactiveMap.toggleButtonState('editFeature', 'disabled', true)
interactiveMap.toggleButtonState('deleteFeature', 'disabled', true)
}
}
]
})
})

interactiveMap.on('draw:started', () => {
interactPlugin.disable()
})

interactiveMap.on('draw:created', () => {
interactiveMap.toggleButtonState('drawTools', 'hidden', false)
interactPlugin.enable()
})

interactiveMap.on('draw:edited', () => {
interactiveMap.toggleButtonState('drawTools', 'hidden', false)
interactPlugin.enable()
})

interactiveMap.on('draw:cancelled', () => {
interactiveMap.toggleButtonState('drawTools', 'hidden', false)
interactPlugin.enable()
})

interactiveMap.on('interact:selectionchange', (e) => {
const singleFeature = e.selectedFeatures.length === 1
const anyFeature = e.selectedFeatures.length > 0
const isDrawFeature = singleFeature && DRAW_LAYERS.includes(e.selectedFeatures[0].layerId)
const allDrawFeatures = anyFeature && e.selectedFeatures.every(f => DRAW_LAYERS.includes(f.layerId))
selectedFeatureIds = e.selectedFeatures.map(f => f.featureId)
interactiveMap.toggleButtonState('drawPolygon', 'disabled', singleFeature)
interactiveMap.toggleButtonState('drawLine', 'disabled', singleFeature)
interactiveMap.toggleButtonState('editFeature', 'disabled', !isDrawFeature)
interactiveMap.toggleButtonState('deleteFeature', 'disabled', !allDrawFeatures)
})
})
}, [])

return <div className='app-no-prose app-example'><div id='demo-map-draw'></div></div>
}

export default function DemoMapDraw () {
return (
<BrowserOnly
fallback={<div className='govuk-inset-text'>The map requires JavaScript to be enabled.</div>}
>
{() => <MapInner />}
</BrowserOnly>
)
}
2 changes: 1 addition & 1 deletion demo/DemoMapMarkerPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function MapInner () {
mapStyle: MAP_STYLE,
center: MARKER_COORDS,
zoom: 15,
containerHeight: '500px',
containerHeight: '516px',
plugins: [interactPlugin]
})

Expand Down
94 changes: 94 additions & 0 deletions demo/DemoMapSearchNominatim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect, useRef } from 'react'
import BrowserOnly from '@docusaurus/BrowserOnly'

const MAP_STYLE = {
url: 'https://labs.os.uk/tiles/styles/open-zoomstack-outdoor/style.json',
attribution: `Contains OS data © Crown copyright and database rights ${new Date().getFullYear()}`,
backgroundColor: '#f5f5f0'
}

const markQuery = (text, query) => {
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return text.replace(new RegExp(`(${escaped})`, 'i'), '<mark>$1</mark>')
}

const SETTLEMENT_TYPES = new Set(['city', 'town', 'village', 'hamlet', 'suburb', 'quarter', 'neighbourhood', 'municipality', 'borough', 'district', 'county'])

const nominatimDataset = {
name: 'nominatim',
buildRequest: (query) => {
const params = new URLSearchParams({
q: query,
format: 'json',
limit: '8',
countrycodes: 'gb'
})
return new Request(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: { 'Accept-Language': 'en' }
})
},
parseResults: (json, query) => {
if (!Array.isArray(json)) {
return []
}
const results = json.filter(item => SETTLEMENT_TYPES.has(item.addresstype)).map(item => {
// Nominatim boundingbox: [min_lat, max_lat, min_lon, max_lon]
const [minLat, maxLat, minLon, maxLon] = item.boundingbox.map(Number)
return {
id: String(item.place_id),
text: item.display_name,
marked: markQuery(item.display_name, query),
point: [Number(item.lon), Number(item.lat)],
bounds: [minLon, minLat, maxLon, maxLat]
}
})
return Array.from(new Map(results.map(r => [r.text, r])).values())
}
}

function MapInner () {
const initialised = useRef(false)

useEffect(() => {
if (initialised.current) {
return
}
initialised.current = true

Promise.all([
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/search/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
{ default: createSearchPlugin }
]) => {
const searchPlugin = createSearchPlugin({
customDatasets: [nominatimDataset]
})

new InteractiveMap('demo-map-search-control', {
behaviour: 'inline',
mapProvider: maplibreProvider(),
mapStyle: MAP_STYLE,
center: [-1.6, 53.1],
zoom: 6,
containerHeight: '516px',
plugins: [searchPlugin]
})
})
}, [])

return <div id='demo-map-search-control' className='app-no-prose app-example'></div>
}

export default function DemoMapSearchNominatim () {
return (
<BrowserOnly
fallback={<div className='govuk-inset-text'>The map requires JavaScript to be enabled.</div>}
>
{() => <MapInner />}
</BrowserOnly>
)
}
96 changes: 96 additions & 0 deletions demo/DemoMapSelectBuilding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useEffect, useRef } from 'react'
import BrowserOnly from '@docusaurus/BrowserOnly'

const MAP_STYLE = {
url: 'https://labs.os.uk/tiles/styles/open-zoomstack-outdoor/style.json',
attribution: `Contains OS data © Crown copyright and database rights ${new Date().getFullYear()}`,
backgroundColor: '#f5f5f0'
}

const CENTER = [-1.5491, 53.8008]
const PANEL_ID = 'building-info'

function MapInner () {
const initialised = useRef(false)

useEffect(() => {
if (initialised.current) {
return
}
initialised.current = true

Promise.all([
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/interact/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
{ default: createInteractPlugin }
]) => {
const interactPlugin = createInteractPlugin({
interactionModes: ['selectFeature'],
deselectOnClickOutside: true,
debug: true,
layers: [
{ layerId: 'buildings 3D', idProperty: 'uuid' }
]
})

const map = new InteractiveMap('demo-map-select-building', {
behaviour: 'inline',
mapProvider: maplibreProvider(),
mapStyle: MAP_STYLE,
center: CENTER,
zoom: 14.5,
maxZoom: 14.5,
containerHeight: '516px',
plugins: [interactPlugin]
})

map.on('map:ready', () => {
interactPlugin.enable()

map.addPanel(PANEL_ID, {
focus: false,
label: 'Selected building',
html: '<div id="building-info-content"></div>',
mobile: { slot: 'drawer', dismissible: true, open: false },
tablet: { slot: 'left-top', dismissible: true, width: '300px', open: false },
desktop: { slot: 'left-top', dismissible: true, width: '300px', open: false }
})
})

map.on('interact:selectionchange', ({ selectedFeatures }) => {
if (selectedFeatures.length > 0) {
let html = ''
for (const [k, v] of Object.entries(selectedFeatures[0].properties)) {
html += `<p class="govuk-body govuk-!-margin-bottom-1"><strong>${k}:</strong> ${v}</p>`
}
document.getElementById('building-info-content').innerHTML = html
map.showPanel(PANEL_ID)
} else {
map.hidePanel(PANEL_ID)
}
})

map.on('app:panelclosed', ({ panelId }) => {
if (panelId === PANEL_ID) {
interactPlugin.clear()
}
})
})
}, [])

return <div id='demo-map-select-building' className='app-no-prose app-example'></div>
}

export default function DemoMapSelectBuilding () {
return (
<BrowserOnly
fallback={<div className='govuk-inset-text'>The map requires JavaScript to be enabled.</div>}
>
{() => <MapInner />}
</BrowserOnly>
)
}
Loading
Loading