Initial Commit for Issue/3819. World map component added and a small change to the Component draggable behaviour to accomodate it.
This commit is contained in:
parent
f7de7ed349
commit
a0588c77ee
|
@ -28,5 +28,8 @@
|
|||
],
|
||||
"rules": {
|
||||
"no-self-assign": "off"
|
||||
},
|
||||
"globals": {
|
||||
"GeolocationPositionError": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,8 @@
|
|||
"link",
|
||||
"icon",
|
||||
"embed",
|
||||
"markdownviewer"
|
||||
"markdownviewer",
|
||||
"embeddedmap"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -2474,6 +2474,82 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"embeddedmap": {
|
||||
"name": "Embedded Map",
|
||||
"icon": "Location",
|
||||
"styles": ["size"],
|
||||
"editable": true,
|
||||
"draggable": false,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
"label": "Provider",
|
||||
"key": "dataProvider"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Map Type",
|
||||
"key": "mapType",
|
||||
"options": [
|
||||
"Roadmap",
|
||||
"Terrain",
|
||||
"Satellite",
|
||||
"Hybrid"
|
||||
],
|
||||
"defaultValue": "Roadmap"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Enable Fullscreen",
|
||||
"key": "fullScreenEnabled",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Enabled Location",
|
||||
"key": "locationEnabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Enable Zoom",
|
||||
"key": "zoomEnabled",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Zoom Level (0-100)",
|
||||
"key": "zoomLevel",
|
||||
"defaultValue": 72,
|
||||
"max" : 100,
|
||||
"min" : 0
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Latitude Key",
|
||||
"key": "latitudeKey"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Longitude Key",
|
||||
"key": "longitudeKey"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Title Key",
|
||||
"key": "titleKey"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Tile URL",
|
||||
"key": "tileURL",
|
||||
"defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"attachmentfield": {
|
||||
"name": "Attachment",
|
||||
"icon": "Attach",
|
||||
|
|
|
@ -32,8 +32,10 @@
|
|||
"@spectrum-css/vars": "^3.0.1",
|
||||
"apexcharts": "^3.22.1",
|
||||
"dayjs": "^1.10.5",
|
||||
"leaflet": "^1.7.1",
|
||||
"regexparam": "^1.3.0",
|
||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"screenfull": "^6.0.1",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte": "^3.38.2",
|
||||
"svelte-apexcharts": "^1.0.2",
|
||||
|
|
|
@ -103,7 +103,12 @@
|
|||
($builderStore.previewType === "layout" || insideScreenslot) &&
|
||||
!isBlock
|
||||
$: editing = editable && selected && $builderStore.editMode
|
||||
$: draggable = !inDragPath && interactive && !isLayout && !isScreen
|
||||
$: draggable =
|
||||
!inDragPath &&
|
||||
interactive &&
|
||||
!isLayout &&
|
||||
!isScreen &&
|
||||
definition?.draggable !== false
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
|
||||
// Empty components are those which accept children but do not have any.
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
<script>
|
||||
import L from "leaflet"
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
import {
|
||||
FullScreenControl,
|
||||
LocationControl,
|
||||
initMapControls,
|
||||
} from "./EmbeddedMapControls"
|
||||
|
||||
initMapControls()
|
||||
|
||||
export let dataProvider
|
||||
export let error
|
||||
export let zoomLevel
|
||||
export let zoomEnabled = true
|
||||
export let latitudeKey = null
|
||||
export let longitudeKey = null
|
||||
export let titleKey = null
|
||||
export let fullScreenEnabled = true
|
||||
export let locationEnabled = true
|
||||
export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
|
||||
const { styleable, notificationStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const embeddedMapId = `${Helpers.uuid()}-wrapper`
|
||||
|
||||
let cachedDeviceCoordinates
|
||||
|
||||
let mapInstance
|
||||
let mapMarkerGroup = new L.FeatureGroup()
|
||||
let mapMarkers = []
|
||||
|
||||
let minZoomLevel = 0
|
||||
let maxZoomLevel = 18
|
||||
let adjustedZoomLevel = !Number.isInteger(zoomLevel)
|
||||
? 72
|
||||
: Math.round(zoomLevel * (maxZoomLevel / 100))
|
||||
|
||||
$: addMapMarkers(
|
||||
mapInstance,
|
||||
dataProvider?.rows,
|
||||
latitudeKey,
|
||||
longitudeKey,
|
||||
titleKey
|
||||
)
|
||||
$: if (typeof mapInstance === "object" && mapMarkers.length > 0) {
|
||||
mapInstance.setZoom(0)
|
||||
mapInstance.fitBounds(mapMarkerGroup.getBounds())
|
||||
}
|
||||
$: zoomControlUpdated(mapInstance, zoomEnabled)
|
||||
$: locationControlUpdated(mapInstance, locationEnabled)
|
||||
$: fullScreenControlUpdated(mapInstance, fullScreenEnabled)
|
||||
$: updateMapDimensions(
|
||||
mapInstance,
|
||||
$component.styles.normal.width,
|
||||
$component.styles.normal.height
|
||||
)
|
||||
|
||||
const updateMapDimensions = mapInstance => {
|
||||
if (typeof mapInstance !== "object") {
|
||||
return
|
||||
}
|
||||
mapInstance.invalidateSize()
|
||||
}
|
||||
|
||||
// Map Button Controls
|
||||
let locationControl = new LocationControl({
|
||||
position: "bottomright",
|
||||
onLocationFail: err => {
|
||||
if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
|
||||
notificationStore.actions.error(
|
||||
"Location requests not permitted. Ensure location is enabled"
|
||||
)
|
||||
} else if (err.code === GeolocationPositionError.POSITION_UNAVAILABLE) {
|
||||
notificationStore.actions.warning(
|
||||
"Location could not be retrieved. Try again"
|
||||
)
|
||||
} else if (err.code === GeolocationPositionError.TIMEOUT) {
|
||||
notificationStore.actions.warning(
|
||||
"Location request timed out. Try again"
|
||||
)
|
||||
} else {
|
||||
notificationStore.actions.error("Unknown location error")
|
||||
}
|
||||
},
|
||||
onLocationSuccess: pos => {
|
||||
cachedDeviceCoordinates = pos
|
||||
if (typeof mapInstance === "object") {
|
||||
mapInstance.setView(cachedDeviceCoordinates, 15)
|
||||
}
|
||||
},
|
||||
})
|
||||
let fullScreenControl = new FullScreenControl({
|
||||
position: "topright",
|
||||
})
|
||||
let zoomControl = L.control.zoom({
|
||||
position: "bottomright",
|
||||
})
|
||||
|
||||
const locationControlUpdated = (mapInstance, locationEnabled) => {
|
||||
if (typeof mapInstance !== "object") {
|
||||
return
|
||||
}
|
||||
if (locationEnabled) {
|
||||
locationControl.addTo(mapInstance)
|
||||
} else {
|
||||
mapInstance.removeControl(locationControl)
|
||||
}
|
||||
}
|
||||
|
||||
const fullScreenControlUpdated = (mapInstance, fullScreenEnabled) => {
|
||||
if (typeof mapInstance !== "object") {
|
||||
return
|
||||
}
|
||||
if (fullScreenEnabled) {
|
||||
fullScreenControl.addTo(mapInstance)
|
||||
} else {
|
||||
mapInstance.removeControl(fullScreenControl)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomControlUpdated = (mapInstance, zoomEnabled) => {
|
||||
if (typeof mapInstance !== "object") {
|
||||
return
|
||||
}
|
||||
if (zoomEnabled) {
|
||||
zoomControl.addTo(mapInstance)
|
||||
mapInstance.scrollWheelZoom.enable()
|
||||
} else {
|
||||
mapInstance.removeControl(zoomControl)
|
||||
mapInstance.scrollWheelZoom.disable()
|
||||
}
|
||||
}
|
||||
|
||||
//Map icon and marker configuration
|
||||
const mapIconMarkup =
|
||||
'<div><svg width="26px" height="26px" class="spectrum-Icon" focusable="false" stroke="#b12b27" stroke-width="1%">' +
|
||||
'<use xlink:href="#spectrum-icon-18-Location" /></svg></div>'
|
||||
const mapIcon = L.divIcon({
|
||||
html: mapIconMarkup,
|
||||
className: "embedded-map-marker",
|
||||
iconSize: [26, 26],
|
||||
iconAnchor: [13, 26],
|
||||
popupAnchor: [0, -13],
|
||||
})
|
||||
const mapMarkerOptions = {
|
||||
icon: mapIcon,
|
||||
draggable: false,
|
||||
alt: "Location Marker",
|
||||
}
|
||||
let mapOptions = {
|
||||
fullScreen: false,
|
||||
zoomControl: false,
|
||||
scrollWheelZoom: zoomEnabled,
|
||||
minZoomLevel,
|
||||
maxZoomLevel,
|
||||
}
|
||||
|
||||
const addMapMarkers = (mapInstance, rows, latKey, lngKey, titleKey) => {
|
||||
if (typeof mapInstance !== "object" || !rows || !latKey || !lngKey) {
|
||||
return
|
||||
}
|
||||
|
||||
mapMarkerGroup.clearLayers()
|
||||
|
||||
let isValidLatitude = value => {
|
||||
return !isNaN(value) && value > -90 && value < 90
|
||||
}
|
||||
|
||||
let isValidLongitude = value => {
|
||||
return !isNaN(value) && value > -180 && value < 180
|
||||
}
|
||||
|
||||
const validRows = rows.filter(row => {
|
||||
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey])
|
||||
})
|
||||
|
||||
validRows.forEach(row => {
|
||||
let markerCoords = [row[latKey], row[lngKey]]
|
||||
|
||||
let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance)
|
||||
let markerContent = generateMarkerPopupContent(
|
||||
row[latKey],
|
||||
row[lngKey],
|
||||
row[titleKey]
|
||||
)
|
||||
|
||||
marker.bindPopup(markerContent).addTo(mapMarkerGroup)
|
||||
|
||||
//https://github.com/Leaflet/Leaflet/issues/7331
|
||||
marker.on("click", function () {
|
||||
this.openPopup()
|
||||
})
|
||||
|
||||
mapMarkers = [...mapMarkers, marker]
|
||||
})
|
||||
}
|
||||
|
||||
const generateMarkerPopupContent = (latitude, longitude, text) => {
|
||||
return text || latitude + "," + longitude
|
||||
}
|
||||
|
||||
const initMap = () => {
|
||||
const initCoords = [51.5072, -0.1276]
|
||||
|
||||
mapInstance = L.map(embeddedMapId, mapOptions)
|
||||
mapMarkerGroup.addTo(mapInstance)
|
||||
|
||||
L.tileLayer(tileURL, {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
zoom: adjustedZoomLevel,
|
||||
}).addTo(mapInstance)
|
||||
|
||||
//Initialise the map
|
||||
mapInstance.setView(initCoords, adjustedZoomLevel)
|
||||
}
|
||||
|
||||
const mapAction = () => {
|
||||
initMap()
|
||||
return {
|
||||
destroy() {
|
||||
mapInstance.remove()
|
||||
mapInstance = undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="embedded-map-wrapper map-default" use:styleable={$component.styles}>
|
||||
{#if error}
|
||||
<div>{error}</div>
|
||||
{/if}
|
||||
|
||||
<div id={embeddedMapId} class="embedded embedded-map" use:mapAction />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.embedded-map-wrapper {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
.map-default {
|
||||
min-height: 180px;
|
||||
min-width: 200px;
|
||||
}
|
||||
.embedded-map :global(.leaflet-top),
|
||||
.embedded-map :global(.leaflet-bottom) {
|
||||
z-index: 998;
|
||||
}
|
||||
.embedded-map :global(.embedded-map-marker) {
|
||||
color: #ee3b35;
|
||||
}
|
||||
.embedded-map :global(.embedded-map-control) {
|
||||
font-size: 22px;
|
||||
}
|
||||
.embedded-map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
:global(.leaflet-tile) {
|
||||
width: 258px !important;
|
||||
height: 258px !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,192 @@
|
|||
import L from "leaflet"
|
||||
import screenfull from "screenfull"
|
||||
|
||||
const createButton = function (html, title, className, container, fn) {
|
||||
let link = L.DomUtil.create("a", className, container)
|
||||
link.innerHTML = html
|
||||
link.href = "#"
|
||||
link.title = title
|
||||
|
||||
link.setAttribute("role", "button")
|
||||
link.setAttribute("aria-label", title)
|
||||
|
||||
L.DomEvent.disableClickPropagation(link)
|
||||
L.DomEvent.on(link, "click", L.DomEvent.stop)
|
||||
L.DomEvent.on(link, "click", fn, this)
|
||||
L.DomEvent.on(link, "click", this._refocusOnMap, this)
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
// Full Screen Control
|
||||
|
||||
const FullScreenControl = L.Control.extend({
|
||||
options: {
|
||||
position: "topright",
|
||||
fullScreenContent:
|
||||
'<span class="embedded-map-control embedded-map-location-icon">' +
|
||||
'<svg width="16px" height="16px" class="spectrum-Icon" focusable="false">' +
|
||||
'<use xlink:href="#spectrum-icon-18-FullScreen" /></svg><span>',
|
||||
fullScreenTitle: "Enter Fullscreen",
|
||||
},
|
||||
onAdd: function () {
|
||||
var fullScreenClassName = "leaflet-control-fullscreen",
|
||||
container = L.DomUtil.create("div", fullScreenClassName + " leaflet-bar"),
|
||||
options = this.options
|
||||
|
||||
this._fullScreenButton = this._createButton(
|
||||
options.fullScreenContent,
|
||||
options.fullScreenTitle,
|
||||
"map-fullscreen",
|
||||
container,
|
||||
this._fullScreen
|
||||
)
|
||||
|
||||
return container
|
||||
},
|
||||
_fullScreen: function () {
|
||||
var map = this._map
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle(map.getContainer())
|
||||
}
|
||||
},
|
||||
_createButton: createButton,
|
||||
})
|
||||
|
||||
const initFullScreenControl = () => {
|
||||
L.Map.mergeOptions({
|
||||
fullScreen: false,
|
||||
})
|
||||
|
||||
L.Map.addInitHook(function () {
|
||||
if (this.options.fullScreen) {
|
||||
this.fullScreenControl = new FullScreenControl()
|
||||
this.addControl(this.fullScreenControl)
|
||||
} else {
|
||||
this.fullScreenControl = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Location Control
|
||||
|
||||
const LocationControl = L.Control.extend({
|
||||
options: {
|
||||
position: "topright",
|
||||
locationContent:
|
||||
'<span class="embedded-map-control embedded-map-location-icon">' +
|
||||
'<svg width="16px" height="16px" class="spectrum-Icon" focusable="false">' +
|
||||
'<use xlink:href="#spectrum-icon-18-Campaign" /></svg><span>',
|
||||
locationTitle: "Show Your Location",
|
||||
},
|
||||
onAdd: function () {
|
||||
var locationClassName = "leaflet-control-location",
|
||||
container = L.DomUtil.create("div", locationClassName + " leaflet-bar"),
|
||||
options = this.options
|
||||
|
||||
this._locationButton = this._createButton(
|
||||
options.locationContent,
|
||||
options.locationTitle,
|
||||
"map-location",
|
||||
container,
|
||||
this._location
|
||||
)
|
||||
|
||||
this._updateDisabled()
|
||||
|
||||
return container
|
||||
},
|
||||
disable: function () {
|
||||
this._disabled = true
|
||||
this._updateDisabled()
|
||||
return this
|
||||
},
|
||||
enable: function () {
|
||||
this._disabled = false
|
||||
this._updateDisabled()
|
||||
return this
|
||||
},
|
||||
_location: function () {
|
||||
if (this._disabled == true) {
|
||||
return
|
||||
}
|
||||
this.disable()
|
||||
|
||||
const success = pos => {
|
||||
this._map.closePopup()
|
||||
if (typeof this.options.onLocationSuccess === "function") {
|
||||
this.options.onLocationSuccess({
|
||||
lat: pos.coords.latitude,
|
||||
lng: pos.coords.longitude,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const error = err => {
|
||||
if (typeof this.options.onLocationFail === "function") {
|
||||
this.options.onLocationFail(err)
|
||||
}
|
||||
}
|
||||
|
||||
this._getPosition()
|
||||
.then(success)
|
||||
.catch(error)
|
||||
.finally(() => {
|
||||
this.enable()
|
||||
})
|
||||
},
|
||||
_getPosition: function () {
|
||||
var options = {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 5000,
|
||||
maximumAge: 30000,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, options)
|
||||
})
|
||||
},
|
||||
|
||||
_createButton: createButton,
|
||||
|
||||
_updateDisabled: function () {
|
||||
let disabledClassName = "leaflet-disabled"
|
||||
L.DomUtil.removeClass(this._locationButton, disabledClassName)
|
||||
this._locationButton.setAttribute("aria-disabled", "false")
|
||||
|
||||
if (this._disabled) {
|
||||
L.DomUtil.addClass(this._locationButton, disabledClassName)
|
||||
this._locationButton.setAttribute("aria-disabled", "true")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const initLocationControl = () => {
|
||||
L.Map.mergeOptions({
|
||||
location: false,
|
||||
onLocationFail: null,
|
||||
onLocationSuccess: null,
|
||||
})
|
||||
|
||||
L.Map.addInitHook(function () {
|
||||
if (this.options.location) {
|
||||
this.localControl = new LocationControl()
|
||||
this.addControl(this.LocationControl)
|
||||
} else {
|
||||
this.localControl = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const initMapControls = () => {
|
||||
initFullScreenControl()
|
||||
initLocationControl()
|
||||
}
|
||||
|
||||
export {
|
||||
initFullScreenControl,
|
||||
initLocationControl,
|
||||
initMapControls,
|
||||
FullScreenControl,
|
||||
LocationControl,
|
||||
}
|
|
@ -31,6 +31,7 @@ export { default as cardstat } from "./CardStat.svelte"
|
|||
export { default as spectrumcard } from "./SpectrumCard.svelte"
|
||||
export { default as tag } from "./Tag.svelte"
|
||||
export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||
export { default as embeddedmap } from "./EmbeddedMap.svelte"
|
||||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./table"
|
||||
|
|
Loading…
Reference in New Issue