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:
Dean 2022-03-08 16:41:21 +00:00
parent db033231e3
commit b67b34928a
8 changed files with 548 additions and 2 deletions

View File

@ -28,5 +28,8 @@
],
"rules": {
"no-self-assign": "off"
},
"globals": {
"GeolocationPositionError": true
}
}

View File

@ -82,7 +82,8 @@
"link",
"icon",
"embed",
"markdownviewer"
"markdownviewer",
"embeddedmap"
]
}
]

View File

@ -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",

View File

@ -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",

View File

@ -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.

View File

@ -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:
'&copy; <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>

View File

@ -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,
}

View File

@ -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"