diff --git a/umap/static/umap/js/controls/actions.js b/umap/static/umap/js/controls/actions.js new file mode 100644 index 00000000..4f07f44f --- /dev/null +++ b/umap/static/umap/js/controls/actions.js @@ -0,0 +1,273 @@ +L.U.BaseAction = L.ToolbarAction.extend({ + initialize: function (map) { + this.map = map + this.options.toolbarIcon = { + className: this.options.className, + tooltip: this.options.tooltip, + } + L.ToolbarAction.prototype.initialize.call(this) + if (this.options.helpMenu && !this.map.helpMenuActions[this.options.className]) + this.map.helpMenuActions[this.options.className] = this + }, +}) + +L.U.ImportAction = L.U.BaseAction.extend({ + options: { + helpMenu: true, + className: 'upload-data dark', + tooltip: `${L._('Import data')} (Ctrl+I)`, + }, + + addHooks: function () { + this.map.importer.open() + }, +}) + +L.U.EditPropertiesAction = L.U.BaseAction.extend({ + options: { + helpMenu: true, + className: 'update-map-settings dark', + tooltip: L._('Edit map settings'), + }, + + addHooks: function () { + this.map.edit() + }, +}) + +L.U.ChangeTileLayerAction = L.U.BaseAction.extend({ + options: { + helpMenu: true, + className: 'dark update-map-tilelayers', + tooltip: L._('Change tilelayers'), + }, + + addHooks: function () { + this.map.updateTileLayers() + }, +}) + +L.U.ManageDatalayersAction = L.U.BaseAction.extend({ + options: { + className: 'dark manage-datalayers', + tooltip: L._('Manage layers'), + }, + + addHooks: function () { + this.map.manageDatalayers() + }, +}) + +L.U.UpdateExtentAction = L.U.BaseAction.extend({ + options: { + className: 'update-map-extent dark', + tooltip: L._('Save this center and zoom'), + }, + + addHooks: function () { + this.map.updateExtent() + }, +}) + +L.U.UpdatePermsAction = L.U.BaseAction.extend({ + options: { + className: 'update-map-permissions dark', + tooltip: L._('Update permissions and editors'), + }, + + addHooks: function () { + this.map.permissions.edit() + }, +}) + +L.U.DrawMarkerAction = L.U.BaseAction.extend({ + options: { + helpMenu: true, + className: 'umap-draw-marker dark', + tooltip: `${L._('Draw a marker')} (Ctrl+M)`, + }, + + addHooks: function () { + this.map.startMarker() + }, +}) + +L.U.DrawPolylineAction = L.U.BaseAction.extend({ + options: { + helpMenu: true, + className: 'umap-draw-polyline dark', + tooltip: `${L._('Draw a polyline')} (Ctrl+L)`, + }, + + addHooks: function () { + this.map.startPolyline() + }, +}) + +L.U.DrawPolygonAction = L.U.BaseAction.extend({ + options: { + helpMenu: true, + className: 'umap-draw-polygon dark', + tooltip: `${L._('Draw a polygon')} (Ctrl+P)`, + }, + + addHooks: function () { + this.map.startPolygon() + }, +}) + +L.U.AddPolylineShapeAction = L.U.BaseAction.extend({ + options: { + className: 'umap-draw-polyline-multi dark', + tooltip: L._('Add a line to the current multi'), + }, + + addHooks: function () { + this.map.editedFeature.editor.newShape() + }, +}) + +L.U.AddPolygonShapeAction = L.U.AddPolylineShapeAction.extend({ + options: { + className: 'umap-draw-polygon-multi dark', + tooltip: L._('Add a polygon to the current multi'), + }, +}) + +L.U.BaseFeatureAction = L.ToolbarAction.extend({ + initialize: function (map, feature, latlng) { + this.map = map + this.feature = feature + this.latlng = latlng + L.ToolbarAction.prototype.initialize.call(this) + this.postInit() + }, + + postInit: function () {}, + + hideToolbar: function () { + this.map.removeLayer(this.toolbar) + }, + + addHooks: function () { + this.onClick({ latlng: this.latlng }) + this.hideToolbar() + }, +}) + +L.U.CreateHoleAction = L.U.BaseFeatureAction.extend({ + options: { + toolbarIcon: { + className: 'umap-new-hole', + tooltip: L._('Start a hole here'), + }, + }, + + onClick: function (e) { + this.feature.startHole(e) + }, +}) + +L.U.ToggleEditAction = L.U.BaseFeatureAction.extend({ + options: { + toolbarIcon: { + className: 'umap-toggle-edit', + tooltip: L._('Toggle edit mode (⇧+Click)'), + }, + }, + + onClick: function (e) { + if (this.feature._toggleEditing) this.feature._toggleEditing(e) // Path + else this.feature.edit(e) // Marker + }, +}) + +L.U.DeleteFeatureAction = L.U.BaseFeatureAction.extend({ + options: { + toolbarIcon: { + className: 'umap-delete-all', + tooltip: L._('Delete this feature'), + }, + }, + + postInit: function () { + if (!this.feature.isMulti()) + this.options.toolbarIcon.className = 'umap-delete-one-of-one' + }, + + onClick: function (e) { + this.feature.confirmDelete(e) + }, +}) + +L.U.DeleteShapeAction = L.U.BaseFeatureAction.extend({ + options: { + toolbarIcon: { + className: 'umap-delete-one-of-multi', + tooltip: L._('Delete this shape'), + }, + }, + + onClick: function (e) { + this.feature.enableEdit().deleteShapeAt(e.latlng) + }, +}) + +L.U.ExtractShapeFromMultiAction = L.U.BaseFeatureAction.extend({ + options: { + toolbarIcon: { + className: 'umap-extract-shape-from-multi', + tooltip: L._('Extract shape to separate feature'), + }, + }, + + onClick: function (e) { + this.feature.isolateShape(e.latlng) + }, +}) + +L.U.BaseVertexAction = L.U.BaseFeatureAction.extend({ + initialize: function (map, feature, latlng, vertex) { + this.vertex = vertex + L.U.BaseFeatureAction.prototype.initialize.call(this, map, feature, latlng) + }, +}) + +L.U.DeleteVertexAction = L.U.BaseVertexAction.extend({ + options: { + toolbarIcon: { + className: 'umap-delete-vertex', + tooltip: L._('Delete this vertex (Alt+Click)'), + }, + }, + + onClick: function () { + this.vertex.delete() + }, +}) + +L.U.SplitLineAction = L.U.BaseVertexAction.extend({ + options: { + toolbarIcon: { + className: 'umap-split-line', + tooltip: L._('Split line'), + }, + }, + + onClick: function () { + this.vertex.split() + }, +}) + +L.U.ContinueLineAction = L.U.BaseVertexAction.extend({ + options: { + toolbarIcon: { + className: 'umap-continue-line', + tooltip: L._('Continue line'), + }, + }, + + onClick: function () { + this.vertex.continue() + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/attribution.js b/umap/static/umap/js/controls/attribution.js new file mode 100644 index 00000000..cfd22ccd --- /dev/null +++ b/umap/static/umap/js/controls/attribution.js @@ -0,0 +1,45 @@ +L.U.AttributionControl = L.Control.Attribution.extend({ + options: { + prefix: '', + }, + + _update: function () { + L.Control.Attribution.prototype._update.call(this) + // Use our how container, so we can hide/show on small screens + const credits = this._container.innerHTML + this._container.innerHTML = '' + const container = L.DomUtil.add( + 'div', + 'attribution-container', + this._container, + credits + ) + if (this._map.options.shortCredit) { + L.DomUtil.add( + 'span', + '', + container, + ` — ${L.Util.toHTML(this._map.options.shortCredit)}` + ) + } + if (this._map.options.captionMenus) { + const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`) + L.DomEvent.on(link, 'click', L.DomEvent.stop) + .on(link, 'click', this._map.displayCaption, this._map) + .on(link, 'dblclick', L.DomEvent.stop) + } + if (window.top === window.self && this._map.options.captionMenus) { + // We are not in iframe mode + L.DomUtil.createLink('', container, ` — ${L._('Home')}`, '/') + } + if (this._map.options.captionMenus) { + L.DomUtil.createLink( + '', + container, + ` — ${L._('Powered by uMap')}`, + 'https://github.com/umap-project/umap/' + ) + } + L.DomUtil.createLink('attribution-toggle', this._container, '') + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/contextMenu.js b/umap/static/umap/js/controls/contextMenu.js new file mode 100644 index 00000000..c144ce7d --- /dev/null +++ b/umap/static/umap/js/controls/contextMenu.js @@ -0,0 +1,17 @@ + +/* + * Make it dynamic + */ +L.U.ContextMenu = L.Map.ContextMenu.extend({ + _createItems: function (e) { + this._map.setContextMenuItems(e) + L.Map.ContextMenu.prototype._createItems.call(this) + }, + + _showAtPoint: function (pt, e) { + this._items = [] + this._container.innerHTML = '' + this._createItems(e) + L.Map.ContextMenu.prototype._showAtPoint.call(this, pt, e) + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/dataLayer.js b/umap/static/umap/js/controls/dataLayer.js new file mode 100644 index 00000000..b74b5cf4 --- /dev/null +++ b/umap/static/umap/js/controls/dataLayer.js @@ -0,0 +1,79 @@ +L.U.DataLayer.include({ + renderLegend: function (container) { + if (this.layer.renderLegend) return this.layer.renderLegend(container) + const color = L.DomUtil.create('span', 'datalayer-color', container) + color.style.backgroundColor = this.getColor() + }, + + renderToolbox: function (container) { + const toggle = L.DomUtil.create('i', 'layer-toggle', container), + zoomTo = L.DomUtil.create('i', 'layer-zoom_to', container), + edit = L.DomUtil.create('i', 'layer-edit show-on-edit', container), + table = L.DomUtil.create('i', 'layer-table-edit show-on-edit', container), + remove = L.DomUtil.create('i', 'layer-delete show-on-edit', container) + zoomTo.title = L._('Zoom to layer extent') + toggle.title = L._('Show/hide layer') + edit.title = L._('Edit') + table.title = L._('Edit properties in a table') + remove.title = L._('Delete layer') + if (this.isReadOnly()) { + L.DomUtil.addClass(container, 'readonly') + } else { + L.DomEvent.on(edit, 'click', this.edit, this) + L.DomEvent.on(table, 'click', this.tableEdit, this) + L.DomEvent.on( + remove, + 'click', + function () { + if (!this.isVisible()) return + if (!confirm(L._('Are you sure you want to delete this layer?'))) return + this._delete() + this.map.ui.closePanel() + }, + this + ) + } + L.DomEvent.on(toggle, 'click', this.toggle, this) + L.DomEvent.on(zoomTo, 'click', this.zoomTo, this) + L.DomUtil.addClass(container, this.getHidableClass()) + L.DomUtil.classIf(container, 'off', !this.isVisible()) + container.dataset.id = L.stamp(this) + }, + + getHidableElements: function () { + return document.querySelectorAll(`.${this.getHidableClass()}`) + }, + + getHidableClass: function () { + return `show_with_datalayer_${L.stamp(this)}` + }, + + propagateRemote: function () { + const els = this.getHidableElements() + for (let i = 0; i < els.length; i++) { + L.DomUtil.classIf(els[i], 'remotelayer', this.isRemoteLayer()) + } + }, + + propagateHide: function () { + const els = this.getHidableElements() + for (let i = 0; i < els.length; i++) { + L.DomUtil.addClass(els[i], 'off') + } + }, + + propagateShow: function () { + this.onceLoaded(function () { + const els = this.getHidableElements() + for (let i = 0; i < els.length; i++) { + L.DomUtil.removeClass(els[i], 'off') + } + }, this) + }, +}) + +L.U.DataLayer.addInitHook(function () { + this.on('hide', this.propagateHide) + this.on('show', this.propagateShow) + if (this.isVisible()) this.propagateShow() +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/dataLayers.js b/umap/static/umap/js/controls/dataLayers.js new file mode 100644 index 00000000..da05c652 --- /dev/null +++ b/umap/static/umap/js/controls/dataLayers.js @@ -0,0 +1,166 @@ +L.U.DataLayersControl = L.Control.extend({ + options: { + position: 'topleft', + }, + + labels: { + zoomToLayer: L._('Zoom to layer extent'), + toggleLayer: L._('Show/hide layer'), + editLayer: L._('Edit'), + }, + + initialize: function (map, options) { + this.map = map + L.Control.prototype.initialize.call(this, options) + }, + + _initLayout: function (map) { + const container = (this._container = L.DomUtil.create( + 'div', + 'leaflet-control-browse umap-control' + )), + actions = L.DomUtil.create('div', 'umap-browse-actions', container) + this._datalayers_container = L.DomUtil.create( + 'ul', + 'umap-browse-datalayers', + actions + ) + + L.DomUtil.createButton( + 'umap-browse-link', + actions, + L._('Browse data'), + map.openBrowser, + map + ) + + const toggleButton = L.DomUtil.createButton( + 'umap-browse-toggle', + container, + L._('See data layers') + ) + L.DomEvent.on(toggleButton, 'click', L.DomEvent.stop) + + map.whenReady(function () { + this.update() + }, this) + + if (L.Browser.pointer) { + L.DomEvent.disableClickPropagation(container) + L.DomEvent.on(container, 'wheel', L.DomEvent.stopPropagation) + L.DomEvent.on(container, 'MozMousePixelScroll', L.DomEvent.stopPropagation) + } + if (!L.Browser.touch) { + L.DomEvent.on( + container, + { + mouseenter: this.expand, + mouseleave: this.collapse, + }, + this + ) + } else { + L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation) + L.DomEvent.on(toggleButton, 'click', L.DomEvent.stop).on( + toggleButton, + 'click', + this.expand, + this + ) + map.on('click', this.collapse, this) + } + + return container + }, + + onAdd: function (map) { + if (!this._container) this._initLayout(map) + if (map.options.datalayersControl === 'expanded') this.expand() + return this._container + }, + + onRemove: function (map) { + this.collapse() + }, + + update: function () { + if (this._datalayers_container && this._map) { + this._datalayers_container.innerHTML = '' + this.map.eachDataLayerReverse(function (datalayer) { + this.addDataLayer(this._datalayers_container, datalayer) + }, this) + } + }, + + expand: function () { + L.DomUtil.addClass(this._container, 'expanded') + }, + + collapse: function () { + if (this.map.options.datalayersControl === 'expanded') return + L.DomUtil.removeClass(this._container, 'expanded') + }, + + addDataLayer: function (container, datalayer, draggable) { + const datalayerLi = L.DomUtil.create('li', '', container) + if (draggable) + L.DomUtil.element( + 'i', + { className: 'drag-handle', title: L._('Drag to reorder') }, + datalayerLi + ) + datalayer.renderToolbox(datalayerLi) + const title = L.DomUtil.add( + 'span', + 'layer-title', + datalayerLi, + datalayer.options.name + ) + + datalayerLi.id = `browse_data_toggle_${L.stamp(datalayer)}` + L.DomUtil.classIf(datalayerLi, 'off', !datalayer.isVisible()) + + title.textContent = datalayer.options.name + }, + + newDataLayer: function () { + const datalayer = this.map.createDataLayer({}) + datalayer.edit() + }, + + openPanel: function () { + if (!this.map.editEnabled) return + const container = L.DomUtil.create('ul', 'umap-browse-datalayers') + this.map.eachDataLayerReverse(function (datalayer) { + this.addDataLayer(container, datalayer, true) + }, this) + const orderable = new L.U.Orderable(container) + orderable.on( + 'drop', + function (e) { + const layer = this.map.datalayers[e.src.dataset.id], + other = this.map.datalayers[e.dst.dataset.id], + minIndex = Math.min(e.initialIndex, e.finalIndex) + if (e.finalIndex === 0) layer.bringToTop() + else if (e.finalIndex > e.initialIndex) layer.insertBefore(other) + else layer.insertAfter(other) + this.map.eachDataLayerReverse((datalayer) => { + if (datalayer.getRank() >= minIndex) datalayer.isDirty = true + }) + this.map.indexDatalayers() + }, + this + ) + + const bar = L.DomUtil.create('div', 'button-bar', container) + L.DomUtil.createButton( + 'show-on-edit block add-datalayer button', + bar, + L._('Add a layer'), + this.newDataLayer, + this + ) + + this.map.ui.openPanel({ data: { html: container }, className: 'dark' }) + }, + }) diff --git a/umap/static/umap/js/controls/drawToolbar.js b/umap/static/umap/js/controls/drawToolbar.js new file mode 100644 index 00000000..dec32395 --- /dev/null +++ b/umap/static/umap/js/controls/drawToolbar.js @@ -0,0 +1,33 @@ +L.U.DrawToolbar = L.Toolbar.Control.extend({ + initialize: function (options) { + L.Toolbar.Control.prototype.initialize.call(this, options) + this.map = this.options.map + this.map.on('seteditedfeature', this.redraw, this) + }, + + appendToContainer: function (container) { + this.options.actions = [] + if (this.map.options.enableMarkerDraw) { + this.options.actions.push(L.U.DrawMarkerAction) + } + if (this.map.options.enablePolylineDraw) { + this.options.actions.push(L.U.DrawPolylineAction) + if (this.map.editedFeature && this.map.editedFeature instanceof L.U.Polyline) { + this.options.actions.push(L.U.AddPolylineShapeAction) + } + } + if (this.map.options.enablePolygonDraw) { + this.options.actions.push(L.U.DrawPolygonAction) + if (this.map.editedFeature && this.map.editedFeature instanceof L.U.Polygon) { + this.options.actions.push(L.U.AddPolygonShapeAction) + } + } + L.Toolbar.Control.prototype.appendToContainer.call(this, container) + }, + + redraw: function () { + const container = this._control.getContainer() + container.innerHTML = '' + this.appendToContainer(container) + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/drop.js b/umap/static/umap/js/controls/drop.js new file mode 100644 index 00000000..763218d3 --- /dev/null +++ b/umap/static/umap/js/controls/drop.js @@ -0,0 +1,45 @@ +L.U.DropControl = L.Class.extend({ + initialize: function (map) { + this.map = map + this.dropzone = map._container + }, + + enable: function () { + L.DomEvent.on(this.dropzone, 'dragenter', this.dragenter, this) + L.DomEvent.on(this.dropzone, 'dragover', this.dragover, this) + L.DomEvent.on(this.dropzone, 'drop', this.drop, this) + L.DomEvent.on(this.dropzone, 'dragleave', this.dragleave, this) + }, + + disable: function () { + L.DomEvent.off(this.dropzone, 'dragenter', this.dragenter, this) + L.DomEvent.off(this.dropzone, 'dragover', this.dragover, this) + L.DomEvent.off(this.dropzone, 'drop', this.drop, this) + L.DomEvent.off(this.dropzone, 'dragleave', this.dragleave, this) + }, + + dragenter: function (e) { + L.DomEvent.stop(e) + this.map.scrollWheelZoom.disable() + this.dropzone.classList.add('umap-dragover') + }, + + dragover: function (e) { + L.DomEvent.stop(e) + }, + + drop: function (e) { + this.map.scrollWheelZoom.enable() + this.dropzone.classList.remove('umap-dragover') + L.DomEvent.stop(e) + for (let i = 0, file; (file = e.dataTransfer.files[i]); i++) { + this.map.processFileToImport(file) + } + this.map.onceDataLoaded(this.map.fitDataBounds) + }, + + dragleave: function () { + this.map.scrollWheelZoom.enable() + this.dropzone.classList.remove('umap-dragover') + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/edit.js b/umap/static/umap/js/controls/edit.js new file mode 100644 index 00000000..6e27b92f --- /dev/null +++ b/umap/static/umap/js/controls/edit.js @@ -0,0 +1,32 @@ +L.U.EditControl = L.Control.extend({ + options: { + position: 'topright', + }, + + onAdd: function (map) { + const container = L.DomUtil.create('div', 'leaflet-control-edit-enable') + const enableEditing = L.DomUtil.createButton( + '', + container, + L._('Edit'), + map.enableEdit, + map + ) + L.DomEvent.on( + enableEditing, + 'mouseover', + function () { + map.ui.tooltip({ + content: `${L._('Switch to edit mode')} (Ctrl+E)`, + anchor: enableEditing, + position: 'bottom', + delay: 750, + duration: 5000, + }) + }, + this + ) + + return container + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/editable.js b/umap/static/umap/js/controls/editable.js new file mode 100644 index 00000000..c07f554a --- /dev/null +++ b/umap/static/umap/js/controls/editable.js @@ -0,0 +1,124 @@ +L.U.Editable = L.Editable.extend({ + initialize: function (map, options) { + L.Editable.prototype.initialize.call(this, map, options) + this.on( + 'editable:drawing:start editable:drawing:click editable:drawing:move', + this.drawingTooltip + ) + this.on('editable:drawing:end', this.closeTooltip) + // Layer for items added by users + this.on('editable:drawing:cancel', (e) => { + if (e.layer._latlngs && e.layer._latlngs.length < e.layer.editor.MIN_VERTEX) + e.layer.del() + if (e.layer instanceof L.U.Marker) e.layer.del() + }) + this.on('editable:drawing:commit', function (e) { + e.layer.isDirty = true + if (this.map.editedFeature !== e.layer) e.layer.edit(e) + }) + this.on('editable:editing', (e) => { + const layer = e.layer + layer.isDirty = true + if (layer._tooltip && layer.isTooltipOpen()) { + layer._tooltip.setLatLng(layer.getCenter()) + layer._tooltip.update() + } + }) + this.on('editable:vertex:ctrlclick', (e) => { + const index = e.vertex.getIndex() + if (index === 0 || (index === e.vertex.getLastIndex() && e.vertex.continue)) + e.vertex.continue() + }) + this.on('editable:vertex:altclick', (e) => { + if (e.vertex.editor.vertexCanBeDeleted(e.vertex)) e.vertex.delete() + }) + this.on('editable:vertex:rawclick', this.onVertexRawClick) + }, + + createPolyline: function (latlngs) { + return new L.U.Polyline(this.map, latlngs, this._getDefaultProperties()) + }, + + createPolygon: function (latlngs) { + return new L.U.Polygon(this.map, latlngs, this._getDefaultProperties()) + }, + + createMarker: function (latlng) { + return new L.U.Marker(this.map, latlng, this._getDefaultProperties()) + }, + + _getDefaultProperties: function () { + const result = {} + if (this.map.options.featuresHaveOwner && this.map.options.hasOwnProperty('user')) { + result.geojson = { properties: { owner: this.map.options.user.id } } + } + return result + }, + + connectCreatedToMap: function (layer) { + // Overrided from Leaflet.Editable + const datalayer = this.map.defaultEditDataLayer() + datalayer.addLayer(layer) + layer.isDirty = true + return layer + }, + + drawingTooltip: function (e) { + if (e.layer instanceof L.Marker && e.type == 'editable:drawing:start') { + this.map.ui.tooltip({ content: L._('Click to add a marker') }) + } + if (!(e.layer instanceof L.Polyline)) { + // only continue with Polylines and Polygons + return + } + + let content = L._('Drawing') + let measure + if (e.layer.editor._drawnLatLngs) { + // when drawing (a Polyline or Polygon) + if (!e.layer.editor._drawnLatLngs.length) { + // when drawing first point + if (e.layer instanceof L.Polygon) { + content = L._('Click to start drawing a polygon') + } else if (e.layer instanceof L.Polyline) { + content = L._('Click to start drawing a line') + } + } else { + const tmpLatLngs = e.layer.editor._drawnLatLngs.slice() + tmpLatLngs.push(e.latlng) + measure = e.layer.getMeasure(tmpLatLngs) + + if (e.layer.editor._drawnLatLngs.length < e.layer.editor.MIN_VERTEX) { + // when drawing second point + content = L._('Click to continue drawing') + } else { + // when drawing third point (or more) + content = L._('Click last point to finish shape') + } + } + } else { + // when moving an existing point + measure = e.layer.getMeasure() + } + if (measure) { + if (e.layer instanceof L.Polygon) { + content += L._(' (area: {measure})', { measure: measure }) + } else if (e.layer instanceof L.Polyline) { + content += L._(' (length: {measure})', { measure: measure }) + } + } + if (content) { + this.map.ui.tooltip({ content: content }) + } + }, + + closeTooltip: function () { + this.map.ui.closeTooltip() + }, + + onVertexRawClick: function (e) { + e.layer.onVertexRawClick(e) + L.DomEvent.stop(e) + e.cancel() + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/embed.js b/umap/static/umap/js/controls/embed.js new file mode 100644 index 00000000..695851d5 --- /dev/null +++ b/umap/static/umap/js/controls/embed.js @@ -0,0 +1,19 @@ +/* Share control */ +L.Control.Embed = L.Control.extend({ + options: { + position: 'topleft', + }, + + onAdd: function (map) { + const container = L.DomUtil.create('div', 'leaflet-control-embed umap-control') + const shareButton = L.DomUtil.createButton( + '', + container, + L._('Embed and share this map'), + map.renderShareBox, + map + ) + L.DomEvent.on(shareButton, 'dblclick', L.DomEvent.stopPropagation) + return container + }, + }) \ No newline at end of file diff --git a/umap/static/umap/js/controls/iframeExporter.js b/umap/static/umap/js/controls/iframeExporter.js new file mode 100644 index 00000000..b0f1e3a6 --- /dev/null +++ b/umap/static/umap/js/controls/iframeExporter.js @@ -0,0 +1,70 @@ +L.U.IframeExporter = L.Evented.extend({ + options: { + includeFullScreenLink: true, + currentView: false, + keepCurrentDatalayers: false, + viewCurrentFeature: false, + }, + + queryString: { + scaleControl: false, + miniMap: false, + scrollWheelZoom: false, + zoomControl: true, + editMode: 'disabled', + moreControl: true, + searchControl: null, + tilelayersControl: null, + embedControl: null, + datalayersControl: true, + onLoadPanel: 'none', + captionBar: false, + captionMenus: true, + }, + + dimensions: { + width: '100%', + height: '300px', + }, + + initialize: function (map) { + this.map = map + this.baseUrl = L.Util.getBaseUrl() + // Use map default, not generic default + this.queryString.onLoadPanel = this.map.options.onLoadPanel + }, + + getMap: function () { + return this.map + }, + + buildUrl: function (options) { + const datalayers = [] + if (this.options.viewCurrentFeature && this.map.currentFeature) { + this.queryString.feature = this.map.currentFeature.getSlug() + } + if (this.options.keepCurrentDatalayers) { + this.map.eachDataLayer((datalayer) => { + if (datalayer.isVisible() && datalayer.umap_id) { + datalayers.push(datalayer.umap_id) + } + }) + this.queryString.datalayers = datalayers.join(',') + } else { + delete this.queryString.datalayers + } + const currentView = this.options.currentView ? window.location.hash : '' + const queryString = L.extend({}, this.queryString, options) + return `${this.baseUrl}?${L.Util.buildQueryString(queryString)}${currentView}` + }, + + build: function () { + const iframeUrl = this.buildUrl() + let code = `` + if (this.options.includeFullScreenLink) { + const fullUrl = this.buildUrl({ scrollWheelZoom: true }) + code += `
` + } + return code + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/loading.js b/umap/static/umap/js/controls/loading.js new file mode 100644 index 00000000..bf27ad43 --- /dev/null +++ b/umap/static/umap/js/controls/loading.js @@ -0,0 +1,16 @@ +L.Control.Loading.include({ + onAdd: function (map) { + this._container = L.DomUtil.create('div', 'umap-loader', map._controlContainer) + map.on('baselayerchange', this._layerAdd, this) + this._addMapListeners(map) + this._map = map + }, + + _showIndicator: function () { + L.DomUtil.addClass(this._map._container, 'umap-loading') + }, + + _hideIndicator: function () { + L.DomUtil.removeClass(this._map._container, 'umap-loading') + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/map.js b/umap/static/umap/js/controls/map.js new file mode 100644 index 00000000..78b9b672 --- /dev/null +++ b/umap/static/umap/js/controls/map.js @@ -0,0 +1,444 @@ +L.U.Map.include({ + _openFacet: function () { + const container = L.DomUtil.create('div', 'umap-facet-search'), + title = L.DomUtil.add('h3', 'umap-filter-title', container, L._('Facet search')), + keys = Object.keys(this.getFacetKeys()) + + const knownValues = {} + + keys.forEach((key) => { + knownValues[key] = [] + if (!this.facets[key]) this.facets[key] = [] + }) + + this.eachBrowsableDataLayer((datalayer) => { + datalayer.eachFeature((feature) => { + keys.forEach((key) => { + let value = feature.properties[key] + if (typeof value !== 'undefined' && !knownValues[key].includes(value)) { + knownValues[key].push(value) + } + }) + }) + }) + + const filterFeatures = function () { + let found = false + this.eachBrowsableDataLayer((datalayer) => { + datalayer.resetLayer(true) + if (datalayer.hasDataVisible()) found = true + }) + // TODO: display a results counter in the panel instead. + if (!found) + this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) + } + + const fields = keys.map((current) => [ + `facets.${current}`, + { + handler: 'FacetSearch', + choices: knownValues[current], + label: this.getFacetKeys()[current], + }, + ]) + const builder = new L.U.FormBuilder(this, fields, { + makeDirty: false, + callback: filterFeatures, + callbackContext: this, + }) + container.appendChild(builder.build()) + + this.ui.openPanel({ data: { html: container }, actions: [this._aboutLink()] }) + }, + + _aboutLink: function () { + const link = L.DomUtil.create('li', '') + L.DomUtil.create('i', 'umap-icon-16 umap-caption', link) + const label = L.DomUtil.create('span', '', link) + label.textContent = label.title = L._('About') + L.DomEvent.on(link, 'click', this.displayCaption, this) + return link + }, + + displayCaption: function () { + const container = L.DomUtil.create('div', 'umap-caption') + let title = L.DomUtil.create('h3', '', container) + title.textContent = this.options.name + this.permissions.addOwnerLink('h5', container) + if (this.options.description) { + const description = L.DomUtil.create('div', 'umap-map-description', container) + description.innerHTML = L.Util.toHTML(this.options.description) + } + const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container) + this.eachVisibleDataLayer((datalayer) => { + if (!datalayer.options.inCaption) return + const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer), + legend = L.DomUtil.create('span', '', p), + headline = L.DomUtil.create('strong', '', p), + description = L.DomUtil.create('span', '', p) + datalayer.onceLoaded(function () { + datalayer.renderLegend(legend) + if (datalayer.options.description) { + description.innerHTML = L.Util.toHTML(datalayer.options.description) + } + }) + datalayer.renderToolbox(headline) + L.DomUtil.add('span', '', headline, `${datalayer.options.name} `) + }) + const creditsContainer = L.DomUtil.create('div', 'credits-container', container), + credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits')) + title = L.DomUtil.add('h5', '', credits, L._('User content credits')) + if (this.options.shortCredit || this.options.longCredit) { + L.DomUtil.add( + 'p', + '', + credits, + L.Util.toHTML(this.options.longCredit || this.options.shortCredit) + ) + } + if (this.options.licence) { + const licence = L.DomUtil.add( + 'p', + '', + credits, + `${L._('Map user content has been published under licence')} ` + ) + L.DomUtil.createLink( + '', + licence, + this.options.licence.name, + this.options.licence.url + ) + } else { + L.DomUtil.add('p', '', credits, L._('No licence has been set')) + } + L.DomUtil.create('hr', '', credits) + title = L.DomUtil.create('h5', '', credits) + title.textContent = L._('Map background credits') + const tilelayerCredit = L.DomUtil.create('p', '', credits), + name = L.DomUtil.create('strong', '', tilelayerCredit), + attribution = L.DomUtil.create('span', '', tilelayerCredit) + name.textContent = `${this.selected_tilelayer.options.name} ` + attribution.innerHTML = this.selected_tilelayer.getAttribution() + L.DomUtil.create('hr', '', credits) + const umapCredit = L.DomUtil.create('p', '', credits), + urls = { + leaflet: 'http://leafletjs.com', + django: 'https://www.djangoproject.com', + umap: 'http://wiki.openstreetmap.org/wiki/UMap', + changelog: 'https://umap-project.readthedocs.io/en/master/changelog/', + version: this.options.umap_version, + } + umapCredit.innerHTML = L._( + ` + Powered by Leaflet and + Django, + glued by uMap project + (version {version}). + `, + urls + ) + const browser = L.DomUtil.create('li', '') + L.DomUtil.create('i', 'umap-icon-16 umap-list', browser) + const labelBrowser = L.DomUtil.create('span', '', browser) + labelBrowser.textContent = labelBrowser.title = L._('Browse data') + L.DomEvent.on(browser, 'click', this.openBrowser, this) + const actions = [browser] + if (this.options.facetKey) { + const filter = L.DomUtil.create('li', '') + L.DomUtil.create('i', 'umap-icon-16 umap-add', filter) + const labelFilter = L.DomUtil.create('span', '', filter) + labelFilter.textContent = labelFilter.title = L._('Facet search') + L.DomEvent.on(filter, 'click', this.openFacet, this) + actions.push(filter) + } + this.ui.openPanel({ data: { html: container }, actions: actions }) + }, + + EXPORT_TYPES: { + geojson: { + formatter: function (map) { + return JSON.stringify(map.toGeoJSON(), null, 2) + }, + ext: '.geojson', + filetype: 'application/json', + }, + gpx: { + formatter: function (map) { + return togpx(map.toGeoJSON()) + }, + ext: '.gpx', + filetype: 'application/gpx+xml', + }, + kml: { + formatter: function (map) { + return tokml(map.toGeoJSON()) + }, + ext: '.kml', + filetype: 'application/vnd.google-earth.kml+xml', + }, + csv: { + formatter: function (map) { + const table = [] + map.eachFeature((feature) => { + const row = feature.toGeoJSON()['properties'], + center = feature.getCenter() + delete row['_umap_options'] + row['Latitude'] = center.lat + row['Longitude'] = center.lng + table.push(row) + }) + return csv2geojson.dsv.csvFormat(table) + }, + ext: '.csv', + filetype: 'text/csv', + }, + }, + + renderEditToolbar: function () { + const container = L.DomUtil.create( + 'div', + 'umap-main-edit-toolbox with-transition dark', + this._controlContainer + ) + const leftContainer = L.DomUtil.create('div', 'umap-left-edit-toolbox', container) + const rightContainer = L.DomUtil.create('div', 'umap-right-edit-toolbox', container) + const logo = L.DomUtil.create('div', 'logo', leftContainer) + L.DomUtil.createLink('', logo, 'uMap', '/', null, L._('Go to the homepage')) + const nameButton = L.DomUtil.createButton( + 'map-name', + leftContainer, + '', + this.edit, + this + ) + L.DomEvent.on( + nameButton, + 'mouseover', + function () { + this.ui.tooltip({ + content: L._('Edit the title of the map'), + anchor: nameButton, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }, + this + ) + const shareStatusButton = L.DomUtil.createButton( + 'share-status', + leftContainer, + '', + this.permissions.edit, + this.permissions + ) + L.DomEvent.on( + shareStatusButton, + 'mouseover', + function () { + this.ui.tooltip({ + content: L._('Update who can see and edit the map'), + anchor: shareStatusButton, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }, + this + ) + const update = () => { + const status = this.permissions.getShareStatusDisplay() + nameButton.textContent = this.getDisplayName() + // status is not set until map is saved once + if (status) { + shareStatusButton.textContent = L._('Visibility: {status}', { + status: status, + }) + } + } + update() + this.once('saved', L.bind(update, this)) + if (this.options.editMode === 'advanced') { + L.DomEvent.on(nameButton, 'click', this.edit, this) + L.DomEvent.on(shareStatusButton, 'click', this.permissions.edit, this.permissions) + } + this.on('postsync', L.bind(update, this)) + if (this.options.user) { + L.DomUtil.createLink( + 'umap-user', + rightContainer, + L._(`My Dashboard ({username})`, { + username: this.options.user.name, + }), + this.options.user.url + ) + } + this.help.link(rightContainer, 'edit') + const controlEditCancel = L.DomUtil.createButton( + 'leaflet-control-edit-cancel', + rightContainer, + L.DomUtil.add('span', '', null, L._('Cancel edits')), + this.askForReset, + this + ) + L.DomEvent.on( + controlEditCancel, + 'mouseover', + function () { + this.ui.tooltip({ + content: `${L._('Cancel')} (Ctrl+Z)`, + anchor: controlEditCancel, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }, + this + ) + const controlEditDisable = L.DomUtil.createButton( + 'leaflet-control-edit-disable', + rightContainer, + L.DomUtil.add('span', '', null, L._('View')), + function (e) { + this.disableEdit(e) + this.ui.closePanel() + }, + this + ) + L.DomEvent.on( + controlEditDisable, + 'mouseover', + function () { + this.ui.tooltip({ + content: `${L._('Back to preview')} (Ctrl+E)`, + anchor: controlEditDisable, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }, + this + ) + const controlEditSave = L.DomUtil.createButton( + 'leaflet-control-edit-save button', + rightContainer, + L.DomUtil.add('span', '', null, L._('Save')), + this.save, + this + ) + L.DomEvent.on( + controlEditSave, + 'mouseover', + function () { + this.ui.tooltip({ + content: `${L._('Save current edits')} (Ctrl+S)`, + anchor: controlEditSave, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }, + this + ) + }, + + renderShareBox: function () { + const container = L.DomUtil.create('div', 'umap-share') + const embedTitle = L.DomUtil.add('h4', '', container, L._('Embed the map')) + const iframe = L.DomUtil.create('textarea', 'umap-share-iframe', container) + const urlTitle = L.DomUtil.add('h4', '', container, L._('Direct link')) + const exportUrl = L.DomUtil.create('input', 'umap-share-url', container) + let option + exportUrl.type = 'text' + const UIFields = [ + ['dimensions.width', { handler: 'Input', label: L._('width') }], + ['dimensions.height', { handler: 'Input', label: L._('height') }], + [ + 'options.includeFullScreenLink', + { handler: 'Switch', label: L._('Include full screen link?') }, + ], + [ + 'options.currentView', + { handler: 'Switch', label: L._('Current view instead of default map view?') }, + ], + [ + 'options.keepCurrentDatalayers', + { handler: 'Switch', label: L._('Keep current visible layers') }, + ], + [ + 'options.viewCurrentFeature', + { handler: 'Switch', label: L._('Open current feature on load') }, + ], + 'queryString.moreControl', + 'queryString.scrollWheelZoom', + 'queryString.miniMap', + 'queryString.scaleControl', + 'queryString.onLoadPanel', + 'queryString.captionBar', + 'queryString.captionMenus', + ] + for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) { + UIFields.push(`queryString.${this.HIDDABLE_CONTROLS[i]}Control`) + } + const iframeExporter = new L.U.IframeExporter(this) + const buildIframeCode = () => { + iframe.innerHTML = iframeExporter.build() + exportUrl.value = window.location.protocol + iframeExporter.buildUrl() + } + buildIframeCode() + const builder = new L.U.FormBuilder(iframeExporter, UIFields, { + callback: buildIframeCode, + }) + const iframeOptions = L.DomUtil.createFieldset(container, L._('Export options')) + iframeOptions.appendChild(builder.build()) + if (this.options.shortUrl) { + L.DomUtil.create('hr', '', container) + L.DomUtil.add('h4', '', container, L._('Short URL')) + const shortUrl = L.DomUtil.create('input', 'umap-short-url', container) + shortUrl.type = 'text' + shortUrl.value = this.options.shortUrl + } + L.DomUtil.create('hr', '', container) + L.DomUtil.add('h4', '', container, L._('Backup data')) + const downloadUrl = L.Util.template(this.options.urls.map_download, { + map_id: this.options.umap_id, + }) + const link = L.DomUtil.createLink( + 'button', + container, + L._('Download full data'), + downloadUrl + ) + let name = this.options.name || 'data' + name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase() + link.setAttribute('download', `${name}.umap`) + L.DomUtil.create('hr', '', container) + L.DomUtil.add('h4', '', container, L._('Download data')) + const typeInput = L.DomUtil.create('select', '', container) + typeInput.name = 'format' + const exportCaveat = L.DomUtil.add( + 'small', + 'help-text', + container, + L._('Only visible features will be downloaded.') + ) + for (const key in this.EXPORT_TYPES) { + if (this.EXPORT_TYPES.hasOwnProperty(key)) { + option = L.DomUtil.create('option', '', typeInput) + option.value = key + option.textContent = this.EXPORT_TYPES[key].name || key + if (this.EXPORT_TYPES[key].selected) option.selected = true + } + } + L.DomUtil.createButton( + 'button', + container, + L._('Download data'), + () => this.download(typeInput.value), + this + ) + this.ui.openPanel({ data: { html: container } }) + }, + +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/minimap.js b/umap/static/umap/js/controls/minimap.js new file mode 100644 index 00000000..07588fc3 --- /dev/null +++ b/umap/static/umap/js/controls/minimap.js @@ -0,0 +1,19 @@ +L.Control.MiniMap.include({ + initialize: function (layer, options) { + L.Util.setOptions(this, options) + this._layer = this._cloneLayer(layer) + }, + + onMainMapBaseLayerChange: function (e) { + const layer = this._cloneLayer(e.layer) + if (this._miniMap.hasLayer(this._layer)) { + this._miniMap.removeLayer(this._layer) + } + this._layer = layer + this._miniMap.addLayer(this._layer) + }, + + _cloneLayer: function (layer) { + return new L.TileLayer(layer._url, L.Util.extend({}, layer.options)) + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/more.js b/umap/static/umap/js/controls/more.js new file mode 100644 index 00000000..eb22adb9 --- /dev/null +++ b/umap/static/umap/js/controls/more.js @@ -0,0 +1,32 @@ +L.U.MoreControls = L.Control.extend({ + options: { + position: 'topleft', + }, + + onAdd: function () { + const container = L.DomUtil.create('div', 'umap-control-text') + const moreButton = L.DomUtil.createButton( + 'umap-control-more', + container, + L._('More controls'), + this.toggle, + this + ) + const lessButton = L.DomUtil.createButton( + 'umap-control-less', + container, + L._('Hide controls'), + this.toggle, + this + ) + return container + }, + + toggle: function () { + const pos = this.getPosition(), + corner = this._map._controlCorners[pos], + className = 'umap-more-controls' + if (L.DomUtil.hasClass(corner, className)) L.DomUtil.removeClass(corner, className) + else L.DomUtil.addClass(corner, className) + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/permanentCredits.js b/umap/static/umap/js/controls/permanentCredits.js new file mode 100644 index 00000000..c1560bb0 --- /dev/null +++ b/umap/static/umap/js/controls/permanentCredits.js @@ -0,0 +1,37 @@ + +L.U.PermanentCreditsControl = L.Control.extend({ + options: { + position: 'bottomleft', + }, + + initialize: function (map, options) { + this.map = map + L.Control.prototype.initialize.call(this, options) + }, + + onAdd: function () { + const paragraphContainer = L.DomUtil.create( + 'div', + 'umap-permanent-credits-container' + ), + creditsParagraph = L.DomUtil.create('p', '', paragraphContainer) + + this.paragraphContainer = paragraphContainer + this.setCredits() + this.setBackground() + + return paragraphContainer + }, + + setCredits: function () { + this.paragraphContainer.innerHTML = L.Util.toHTML(this.map.options.permanentCredit) + }, + + setBackground: function () { + if (this.map.options.permanentCreditBackground) { + this.paragraphContainer.style.backgroundColor = '#FFFFFFB0' + } else { + this.paragraphContainer.style.backgroundColor = '' + } + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/search.js b/umap/static/umap/js/controls/search.js new file mode 100644 index 00000000..3b56f3b1 --- /dev/null +++ b/umap/static/umap/js/controls/search.js @@ -0,0 +1,98 @@ +L.U.Search = L.PhotonSearch.extend({ + initialize: function (map, input, options) { + L.PhotonSearch.prototype.initialize.call(this, map, input, options) + this.options.url = map.options.urls.search + if (map.options.maxBounds) this.options.bbox = map.options.maxBounds.toBBoxString() + }, + + onBlur: function (e) { + // Overrided because we don't want to hide the results on blur. + this.fire('blur') + }, + + formatResult: function (feature, el) { + const self = this + const tools = L.DomUtil.create('span', 'search-result-tools', el), + zoom = L.DomUtil.create('i', 'feature-zoom_to', tools), + edit = L.DomUtil.create('i', 'feature-edit show-on-edit', tools) + zoom.title = L._('Zoom to this place') + edit.title = L._('Save this location as new feature') + // We need to use "mousedown" because Leaflet.Photon listen to mousedown + // on el. + L.DomEvent.on(zoom, 'mousedown', (e) => { + L.DomEvent.stop(e) + self.zoomToFeature(feature) + }) + L.DomEvent.on(edit, 'mousedown', (e) => { + L.DomEvent.stop(e) + const datalayer = self.map.defaultEditDataLayer() + const layer = datalayer.geojsonToFeatures(feature) + layer.isDirty = true + layer.edit() + }) + this._formatResult(feature, el) + }, + + zoomToFeature: function (feature) { + const zoom = Math.max(this.map.getZoom(), 16) // Never unzoom. + this.map.setView( + [feature.geometry.coordinates[1], feature.geometry.coordinates[0]], + zoom + ) + }, + + onSelected: function (feature) { + this.zoomToFeature(feature) + this.map.ui.closePanel() + }, +}) + + +L.U.SearchControl = L.Control.extend({ + options: { + position: 'topleft', + }, + + onAdd: function (map) { + const container = L.DomUtil.create('div', 'leaflet-control-search umap-control') + L.DomEvent.disableClickPropagation(container) + L.DomUtil.createButton( + '', + container, + L._('Search a place name'), + (e) => { + L.DomEvent.stop(e) + this.openPanel(map) + }, + this + ) + return container + }, + + openPanel: function (map) { + const options = { + limit: 10, + noResultLabel: L._('No results'), + } + if (map.options.photonUrl) options.url = map.options.photonUrl + const container = L.DomUtil.create('div', '') + + const title = L.DomUtil.create('h3', '', container) + title.textContent = L._('Search location') + const input = L.DomUtil.create('input', 'photon-input', container) + const resultsContainer = L.DomUtil.create('div', 'photon-autocomplete', container) + this.search = new L.U.Search(map, input, options) + const id = Math.random() + this.search.on('ajax:send', () => { + map.fire('dataloading', { id: id }) + }) + this.search.on('ajax:return', () => { + map.fire('dataload', { id: id }) + }) + this.search.resultsContainer = resultsContainer + map.ui.once('panel:ready', () => { + input.focus() + }) + map.ui.openPanel({ data: { html: container } }) + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/settings.js b/umap/static/umap/js/controls/settings.js new file mode 100644 index 00000000..b3e1247e --- /dev/null +++ b/umap/static/umap/js/controls/settings.js @@ -0,0 +1,7 @@ +// Leaflet.Toolbar doesn't allow twice same toolbar class… +L.U.SettingsToolbar = L.Toolbar.Control.extend({ + addTo: function (map) { + if (map.options.editMode !== 'advanced') return + L.Toolbar.Control.prototype.addTo.call(this, map) + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/star.js b/umap/static/umap/js/controls/star.js new file mode 100644 index 00000000..8cb64c0d --- /dev/null +++ b/umap/static/umap/js/controls/star.js @@ -0,0 +1,22 @@ +L.U.StarControl = L.Control.extend({ + options: { + position: 'topleft', + }, + + onAdd: function (map) { + const status = map.options.starred ? ' starred' : '' + const container = L.DomUtil.create( + 'div', + `leaflet-control-star umap-control${status}` + ) + const starMapButton = L.DomUtil.createButton( + '', + container, + L._('Star this map'), + map.star, + map + ) + L.DomEvent.on(starMapButton, 'dblclick', L.DomEvent.stopPropagation) + return container + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/controls/tileLayer.js b/umap/static/umap/js/controls/tileLayer.js new file mode 100644 index 00000000..d7108bbb --- /dev/null +++ b/umap/static/umap/js/controls/tileLayer.js @@ -0,0 +1,65 @@ +L.U.TileLayerControl = L.Control.extend({ + options: { + position: 'topleft', + }, + + initialize: function (map, options) { + this.map = map + L.Control.prototype.initialize.call(this, options) + }, + + onAdd: function () { + const container = L.DomUtil.create('div', 'leaflet-control-tilelayers umap-control') + const changeMapBackgroundButton = L.DomUtil.createButton( + '', + container, + L._('Change map background'), + this.openSwitcher, + this + ) + L.DomEvent.on(changeMapBackgroundButton, 'dblclick', L.DomEvent.stopPropagation) + return container + }, + + openSwitcher: function (options) { + this._tilelayers_container = L.DomUtil.create( + 'ul', + 'umap-tilelayer-switcher-container' + ) + this.buildList(options) + }, + + buildList: function (options) { + this.map.eachTileLayer(function (tilelayer) { + if ( + window.location.protocol === 'https:' && + tilelayer.options.url_template.indexOf('http:') === 0 + ) + return + this.addTileLayerElement(tilelayer, options) + }, this) + this.map.ui.openPanel({ + data: { html: this._tilelayers_container }, + className: options.className, + }) + }, + + addTileLayerElement: function (tilelayer, options) { + const selectedClass = this.map.hasLayer(tilelayer) ? 'selected' : '', + el = L.DomUtil.create('li', selectedClass, this._tilelayers_container), + img = L.DomUtil.create('img', '', el), + name = L.DomUtil.create('div', '', el) + img.src = L.Util.template(tilelayer.options.url_template, this.map.demoTileInfos) + img.loading = 'lazy' + name.textContent = tilelayer.options.name + L.DomEvent.on( + el, + 'click', + function () { + this.map.selectTileLayer(tilelayer) + if (options && options.callback) options.callback(tilelayer) + }, + this + ) + }, +}) \ No newline at end of file diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index ad6cff9f..e69de29b 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1,1658 +0,0 @@ -L.U.BaseAction = L.ToolbarAction.extend({ - initialize: function (map) { - this.map = map - this.options.toolbarIcon = { - className: this.options.className, - tooltip: this.options.tooltip, - } - L.ToolbarAction.prototype.initialize.call(this) - if (this.options.helpMenu && !this.map.helpMenuActions[this.options.className]) - this.map.helpMenuActions[this.options.className] = this - }, -}) - -L.U.ImportAction = L.U.BaseAction.extend({ - options: { - helpMenu: true, - className: 'upload-data dark', - tooltip: `${L._('Import data')} (Ctrl+I)`, - }, - - addHooks: function () { - this.map.importer.open() - }, -}) - -L.U.EditPropertiesAction = L.U.BaseAction.extend({ - options: { - helpMenu: true, - className: 'update-map-settings dark', - tooltip: L._('Edit map settings'), - }, - - addHooks: function () { - this.map.edit() - }, -}) - -L.U.ChangeTileLayerAction = L.U.BaseAction.extend({ - options: { - helpMenu: true, - className: 'dark update-map-tilelayers', - tooltip: L._('Change tilelayers'), - }, - - addHooks: function () { - this.map.updateTileLayers() - }, -}) - -L.U.ManageDatalayersAction = L.U.BaseAction.extend({ - options: { - className: 'dark manage-datalayers', - tooltip: L._('Manage layers'), - }, - - addHooks: function () { - this.map.manageDatalayers() - }, -}) - -L.U.UpdateExtentAction = L.U.BaseAction.extend({ - options: { - className: 'update-map-extent dark', - tooltip: L._('Save this center and zoom'), - }, - - addHooks: function () { - this.map.updateExtent() - }, -}) - -L.U.UpdatePermsAction = L.U.BaseAction.extend({ - options: { - className: 'update-map-permissions dark', - tooltip: L._('Update permissions and editors'), - }, - - addHooks: function () { - this.map.permissions.edit() - }, -}) - -L.U.DrawMarkerAction = L.U.BaseAction.extend({ - options: { - helpMenu: true, - className: 'umap-draw-marker dark', - tooltip: `${L._('Draw a marker')} (Ctrl+M)`, - }, - - addHooks: function () { - this.map.startMarker() - }, -}) - -L.U.DrawPolylineAction = L.U.BaseAction.extend({ - options: { - helpMenu: true, - className: 'umap-draw-polyline dark', - tooltip: `${L._('Draw a polyline')} (Ctrl+L)`, - }, - - addHooks: function () { - this.map.startPolyline() - }, -}) - -L.U.DrawPolygonAction = L.U.BaseAction.extend({ - options: { - helpMenu: true, - className: 'umap-draw-polygon dark', - tooltip: `${L._('Draw a polygon')} (Ctrl+P)`, - }, - - addHooks: function () { - this.map.startPolygon() - }, -}) - -L.U.AddPolylineShapeAction = L.U.BaseAction.extend({ - options: { - className: 'umap-draw-polyline-multi dark', - tooltip: L._('Add a line to the current multi'), - }, - - addHooks: function () { - this.map.editedFeature.editor.newShape() - }, -}) - -L.U.AddPolygonShapeAction = L.U.AddPolylineShapeAction.extend({ - options: { - className: 'umap-draw-polygon-multi dark', - tooltip: L._('Add a polygon to the current multi'), - }, -}) - -L.U.BaseFeatureAction = L.ToolbarAction.extend({ - initialize: function (map, feature, latlng) { - this.map = map - this.feature = feature - this.latlng = latlng - L.ToolbarAction.prototype.initialize.call(this) - this.postInit() - }, - - postInit: function () {}, - - hideToolbar: function () { - this.map.removeLayer(this.toolbar) - }, - - addHooks: function () { - this.onClick({ latlng: this.latlng }) - this.hideToolbar() - }, -}) - -L.U.CreateHoleAction = L.U.BaseFeatureAction.extend({ - options: { - toolbarIcon: { - className: 'umap-new-hole', - tooltip: L._('Start a hole here'), - }, - }, - - onClick: function (e) { - this.feature.startHole(e) - }, -}) - -L.U.ToggleEditAction = L.U.BaseFeatureAction.extend({ - options: { - toolbarIcon: { - className: 'umap-toggle-edit', - tooltip: L._('Toggle edit mode (⇧+Click)'), - }, - }, - - onClick: function (e) { - if (this.feature._toggleEditing) this.feature._toggleEditing(e) // Path - else this.feature.edit(e) // Marker - }, -}) - -L.U.DeleteFeatureAction = L.U.BaseFeatureAction.extend({ - options: { - toolbarIcon: { - className: 'umap-delete-all', - tooltip: L._('Delete this feature'), - }, - }, - - postInit: function () { - if (!this.feature.isMulti()) - this.options.toolbarIcon.className = 'umap-delete-one-of-one' - }, - - onClick: function (e) { - this.feature.confirmDelete(e) - }, -}) - -L.U.DeleteShapeAction = L.U.BaseFeatureAction.extend({ - options: { - toolbarIcon: { - className: 'umap-delete-one-of-multi', - tooltip: L._('Delete this shape'), - }, - }, - - onClick: function (e) { - this.feature.enableEdit().deleteShapeAt(e.latlng) - }, -}) - -L.U.ExtractShapeFromMultiAction = L.U.BaseFeatureAction.extend({ - options: { - toolbarIcon: { - className: 'umap-extract-shape-from-multi', - tooltip: L._('Extract shape to separate feature'), - }, - }, - - onClick: function (e) { - this.feature.isolateShape(e.latlng) - }, -}) - -L.U.BaseVertexAction = L.U.BaseFeatureAction.extend({ - initialize: function (map, feature, latlng, vertex) { - this.vertex = vertex - L.U.BaseFeatureAction.prototype.initialize.call(this, map, feature, latlng) - }, -}) - -L.U.DeleteVertexAction = L.U.BaseVertexAction.extend({ - options: { - toolbarIcon: { - className: 'umap-delete-vertex', - tooltip: L._('Delete this vertex (Alt+Click)'), - }, - }, - - onClick: function () { - this.vertex.delete() - }, -}) - -L.U.SplitLineAction = L.U.BaseVertexAction.extend({ - options: { - toolbarIcon: { - className: 'umap-split-line', - tooltip: L._('Split line'), - }, - }, - - onClick: function () { - this.vertex.split() - }, -}) - -L.U.ContinueLineAction = L.U.BaseVertexAction.extend({ - options: { - toolbarIcon: { - className: 'umap-continue-line', - tooltip: L._('Continue line'), - }, - }, - - onClick: function () { - this.vertex.continue() - }, -}) - -// Leaflet.Toolbar doesn't allow twice same toolbar class… -L.U.SettingsToolbar = L.Toolbar.Control.extend({ - addTo: function (map) { - if (map.options.editMode !== 'advanced') return - L.Toolbar.Control.prototype.addTo.call(this, map) - }, -}) -L.U.DrawToolbar = L.Toolbar.Control.extend({ - initialize: function (options) { - L.Toolbar.Control.prototype.initialize.call(this, options) - this.map = this.options.map - this.map.on('seteditedfeature', this.redraw, this) - }, - - appendToContainer: function (container) { - this.options.actions = [] - if (this.map.options.enableMarkerDraw) { - this.options.actions.push(L.U.DrawMarkerAction) - } - if (this.map.options.enablePolylineDraw) { - this.options.actions.push(L.U.DrawPolylineAction) - if (this.map.editedFeature && this.map.editedFeature instanceof L.U.Polyline) { - this.options.actions.push(L.U.AddPolylineShapeAction) - } - } - if (this.map.options.enablePolygonDraw) { - this.options.actions.push(L.U.DrawPolygonAction) - if (this.map.editedFeature && this.map.editedFeature instanceof L.U.Polygon) { - this.options.actions.push(L.U.AddPolygonShapeAction) - } - } - L.Toolbar.Control.prototype.appendToContainer.call(this, container) - }, - - redraw: function () { - const container = this._control.getContainer() - container.innerHTML = '' - this.appendToContainer(container) - }, -}) - -L.U.DropControl = L.Class.extend({ - initialize: function (map) { - this.map = map - this.dropzone = map._container - }, - - enable: function () { - L.DomEvent.on(this.dropzone, 'dragenter', this.dragenter, this) - L.DomEvent.on(this.dropzone, 'dragover', this.dragover, this) - L.DomEvent.on(this.dropzone, 'drop', this.drop, this) - L.DomEvent.on(this.dropzone, 'dragleave', this.dragleave, this) - }, - - disable: function () { - L.DomEvent.off(this.dropzone, 'dragenter', this.dragenter, this) - L.DomEvent.off(this.dropzone, 'dragover', this.dragover, this) - L.DomEvent.off(this.dropzone, 'drop', this.drop, this) - L.DomEvent.off(this.dropzone, 'dragleave', this.dragleave, this) - }, - - dragenter: function (e) { - L.DomEvent.stop(e) - this.map.scrollWheelZoom.disable() - this.dropzone.classList.add('umap-dragover') - }, - - dragover: function (e) { - L.DomEvent.stop(e) - }, - - drop: function (e) { - this.map.scrollWheelZoom.enable() - this.dropzone.classList.remove('umap-dragover') - L.DomEvent.stop(e) - for (let i = 0, file; (file = e.dataTransfer.files[i]); i++) { - this.map.processFileToImport(file) - } - this.map.onceDataLoaded(this.map.fitDataBounds) - }, - - dragleave: function () { - this.map.scrollWheelZoom.enable() - this.dropzone.classList.remove('umap-dragover') - }, -}) - -L.U.EditControl = L.Control.extend({ - options: { - position: 'topright', - }, - - onAdd: function (map) { - const container = L.DomUtil.create('div', 'leaflet-control-edit-enable') - const enableEditing = L.DomUtil.createButton( - '', - container, - L._('Edit'), - map.enableEdit, - map - ) - L.DomEvent.on( - enableEditing, - 'mouseover', - function () { - map.ui.tooltip({ - content: `${L._('Switch to edit mode')} (Ctrl+E)`, - anchor: enableEditing, - position: 'bottom', - delay: 750, - duration: 5000, - }) - }, - this - ) - - return container - }, -}) - -/* Share control */ -L.Control.Embed = L.Control.extend({ - options: { - position: 'topleft', - }, - - onAdd: function (map) { - const container = L.DomUtil.create('div', 'leaflet-control-embed umap-control') - const shareButton = L.DomUtil.createButton( - '', - container, - L._('Embed and share this map'), - map.renderShareBox, - map - ) - L.DomEvent.on(shareButton, 'dblclick', L.DomEvent.stopPropagation) - return container - }, -}) - -L.U.MoreControls = L.Control.extend({ - options: { - position: 'topleft', - }, - - onAdd: function () { - const container = L.DomUtil.create('div', 'umap-control-text') - const moreButton = L.DomUtil.createButton( - 'umap-control-more', - container, - L._('More controls'), - this.toggle, - this - ) - const lessButton = L.DomUtil.createButton( - 'umap-control-less', - container, - L._('Hide controls'), - this.toggle, - this - ) - return container - }, - - toggle: function () { - const pos = this.getPosition(), - corner = this._map._controlCorners[pos], - className = 'umap-more-controls' - if (L.DomUtil.hasClass(corner, className)) L.DomUtil.removeClass(corner, className) - else L.DomUtil.addClass(corner, className) - }, -}) - -L.U.PermanentCreditsControl = L.Control.extend({ - options: { - position: 'bottomleft', - }, - - initialize: function (map, options) { - this.map = map - L.Control.prototype.initialize.call(this, options) - }, - - onAdd: function () { - const paragraphContainer = L.DomUtil.create( - 'div', - 'umap-permanent-credits-container' - ), - creditsParagraph = L.DomUtil.create('p', '', paragraphContainer) - - this.paragraphContainer = paragraphContainer - this.setCredits() - this.setBackground() - - return paragraphContainer - }, - - setCredits: function () { - this.paragraphContainer.innerHTML = L.Util.toHTML(this.map.options.permanentCredit) - }, - - setBackground: function () { - if (this.map.options.permanentCreditBackground) { - this.paragraphContainer.style.backgroundColor = '#FFFFFFB0' - } else { - this.paragraphContainer.style.backgroundColor = '' - } - }, -}) - -L.U.DataLayersControl = L.Control.extend({ - options: { - position: 'topleft', - }, - - labels: { - zoomToLayer: L._('Zoom to layer extent'), - toggleLayer: L._('Show/hide layer'), - editLayer: L._('Edit'), - }, - - initialize: function (map, options) { - this.map = map - L.Control.prototype.initialize.call(this, options) - }, - - _initLayout: function (map) { - const container = (this._container = L.DomUtil.create( - 'div', - 'leaflet-control-browse umap-control' - )), - actions = L.DomUtil.create('div', 'umap-browse-actions', container) - this._datalayers_container = L.DomUtil.create( - 'ul', - 'umap-browse-datalayers', - actions - ) - - L.DomUtil.createButton( - 'umap-browse-link', - actions, - L._('Browse data'), - map.openBrowser, - map - ) - - const toggleButton = L.DomUtil.createButton( - 'umap-browse-toggle', - container, - L._('See data layers') - ) - L.DomEvent.on(toggleButton, 'click', L.DomEvent.stop) - - map.whenReady(function () { - this.update() - }, this) - - if (L.Browser.pointer) { - L.DomEvent.disableClickPropagation(container) - L.DomEvent.on(container, 'wheel', L.DomEvent.stopPropagation) - L.DomEvent.on(container, 'MozMousePixelScroll', L.DomEvent.stopPropagation) - } - if (!L.Browser.touch) { - L.DomEvent.on( - container, - { - mouseenter: this.expand, - mouseleave: this.collapse, - }, - this - ) - } else { - L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation) - L.DomEvent.on(toggleButton, 'click', L.DomEvent.stop).on( - toggleButton, - 'click', - this.expand, - this - ) - map.on('click', this.collapse, this) - } - - return container - }, - - onAdd: function (map) { - if (!this._container) this._initLayout(map) - if (map.options.datalayersControl === 'expanded') this.expand() - return this._container - }, - - onRemove: function (map) { - this.collapse() - }, - - update: function () { - if (this._datalayers_container && this._map) { - this._datalayers_container.innerHTML = '' - this.map.eachDataLayerReverse(function (datalayer) { - this.addDataLayer(this._datalayers_container, datalayer) - }, this) - } - }, - - expand: function () { - L.DomUtil.addClass(this._container, 'expanded') - }, - - collapse: function () { - if (this.map.options.datalayersControl === 'expanded') return - L.DomUtil.removeClass(this._container, 'expanded') - }, - - addDataLayer: function (container, datalayer, draggable) { - const datalayerLi = L.DomUtil.create('li', '', container) - if (draggable) - L.DomUtil.element( - 'i', - { className: 'drag-handle', title: L._('Drag to reorder') }, - datalayerLi - ) - datalayer.renderToolbox(datalayerLi) - const title = L.DomUtil.add( - 'span', - 'layer-title', - datalayerLi, - datalayer.options.name - ) - - datalayerLi.id = `browse_data_toggle_${L.stamp(datalayer)}` - L.DomUtil.classIf(datalayerLi, 'off', !datalayer.isVisible()) - - title.textContent = datalayer.options.name - }, - - newDataLayer: function () { - const datalayer = this.map.createDataLayer({}) - datalayer.edit() - }, - - openPanel: function () { - if (!this.map.editEnabled) return - const container = L.DomUtil.create('ul', 'umap-browse-datalayers') - this.map.eachDataLayerReverse(function (datalayer) { - this.addDataLayer(container, datalayer, true) - }, this) - const orderable = new L.U.Orderable(container) - orderable.on( - 'drop', - function (e) { - const layer = this.map.datalayers[e.src.dataset.id], - other = this.map.datalayers[e.dst.dataset.id], - minIndex = Math.min(e.initialIndex, e.finalIndex) - if (e.finalIndex === 0) layer.bringToTop() - else if (e.finalIndex > e.initialIndex) layer.insertBefore(other) - else layer.insertAfter(other) - this.map.eachDataLayerReverse((datalayer) => { - if (datalayer.getRank() >= minIndex) datalayer.isDirty = true - }) - this.map.indexDatalayers() - }, - this - ) - - const bar = L.DomUtil.create('div', 'button-bar', container) - L.DomUtil.createButton( - 'show-on-edit block add-datalayer button', - bar, - L._('Add a layer'), - this.newDataLayer, - this - ) - - this.map.ui.openPanel({ data: { html: container }, className: 'dark' }) - }, -}) - -L.U.DataLayer.include({ - renderLegend: function (container) { - if (this.layer.renderLegend) return this.layer.renderLegend(container) - const color = L.DomUtil.create('span', 'datalayer-color', container) - color.style.backgroundColor = this.getColor() - }, - - renderToolbox: function (container) { - const toggle = L.DomUtil.create('i', 'layer-toggle', container), - zoomTo = L.DomUtil.create('i', 'layer-zoom_to', container), - edit = L.DomUtil.create('i', 'layer-edit show-on-edit', container), - table = L.DomUtil.create('i', 'layer-table-edit show-on-edit', container), - remove = L.DomUtil.create('i', 'layer-delete show-on-edit', container) - zoomTo.title = L._('Zoom to layer extent') - toggle.title = L._('Show/hide layer') - edit.title = L._('Edit') - table.title = L._('Edit properties in a table') - remove.title = L._('Delete layer') - if (this.isReadOnly()) { - L.DomUtil.addClass(container, 'readonly') - } else { - L.DomEvent.on(edit, 'click', this.edit, this) - L.DomEvent.on(table, 'click', this.tableEdit, this) - L.DomEvent.on( - remove, - 'click', - function () { - if (!this.isVisible()) return - if (!confirm(L._('Are you sure you want to delete this layer?'))) return - this._delete() - this.map.ui.closePanel() - }, - this - ) - } - L.DomEvent.on(toggle, 'click', this.toggle, this) - L.DomEvent.on(zoomTo, 'click', this.zoomTo, this) - L.DomUtil.addClass(container, this.getHidableClass()) - L.DomUtil.classIf(container, 'off', !this.isVisible()) - container.dataset.id = L.stamp(this) - }, - - getHidableElements: function () { - return document.querySelectorAll(`.${this.getHidableClass()}`) - }, - - getHidableClass: function () { - return `show_with_datalayer_${L.stamp(this)}` - }, - - propagateRemote: function () { - const els = this.getHidableElements() - for (let i = 0; i < els.length; i++) { - L.DomUtil.classIf(els[i], 'remotelayer', this.isRemoteLayer()) - } - }, - - propagateHide: function () { - const els = this.getHidableElements() - for (let i = 0; i < els.length; i++) { - L.DomUtil.addClass(els[i], 'off') - } - }, - - propagateShow: function () { - this.onceLoaded(function () { - const els = this.getHidableElements() - for (let i = 0; i < els.length; i++) { - L.DomUtil.removeClass(els[i], 'off') - } - }, this) - }, -}) - -L.U.DataLayer.addInitHook(function () { - this.on('hide', this.propagateHide) - this.on('show', this.propagateShow) - if (this.isVisible()) this.propagateShow() -}) - -L.U.Map.include({ - _openFacet: function () { - const container = L.DomUtil.create('div', 'umap-facet-search'), - title = L.DomUtil.add('h3', 'umap-filter-title', container, L._('Facet search')), - keys = Object.keys(this.getFacetKeys()) - - const knownValues = {} - - keys.forEach((key) => { - knownValues[key] = [] - if (!this.facets[key]) this.facets[key] = [] - }) - - this.eachBrowsableDataLayer((datalayer) => { - datalayer.eachFeature((feature) => { - keys.forEach((key) => { - let value = feature.properties[key] - if (typeof value !== 'undefined' && !knownValues[key].includes(value)) { - knownValues[key].push(value) - } - }) - }) - }) - - const filterFeatures = function () { - let found = false - this.eachBrowsableDataLayer((datalayer) => { - datalayer.resetLayer(true) - if (datalayer.hasDataVisible()) found = true - }) - // TODO: display a results counter in the panel instead. - if (!found) - this.ui.alert({ content: L._('No results for these facets'), level: 'info' }) - } - - const fields = keys.map((current) => [ - `facets.${current}`, - { - handler: 'FacetSearch', - choices: knownValues[current], - label: this.getFacetKeys()[current], - }, - ]) - const builder = new L.U.FormBuilder(this, fields, { - makeDirty: false, - callback: filterFeatures, - callbackContext: this, - }) - container.appendChild(builder.build()) - - this.ui.openPanel({ data: { html: container }, actions: [this._aboutLink()] }) - }, - - _aboutLink: function () { - const link = L.DomUtil.create('li', '') - L.DomUtil.create('i', 'umap-icon-16 umap-caption', link) - const label = L.DomUtil.create('span', '', link) - label.textContent = label.title = L._('About') - L.DomEvent.on(link, 'click', this.displayCaption, this) - return link - }, - - displayCaption: function () { - const container = L.DomUtil.create('div', 'umap-caption') - let title = L.DomUtil.create('h3', '', container) - title.textContent = this.options.name - this.permissions.addOwnerLink('h5', container) - if (this.options.description) { - const description = L.DomUtil.create('div', 'umap-map-description', container) - description.innerHTML = L.Util.toHTML(this.options.description) - } - const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container) - this.eachVisibleDataLayer((datalayer) => { - if (!datalayer.options.inCaption) return - const p = L.DomUtil.create('p', 'datalayer-legend', datalayerContainer), - legend = L.DomUtil.create('span', '', p), - headline = L.DomUtil.create('strong', '', p), - description = L.DomUtil.create('span', '', p) - datalayer.onceLoaded(function () { - datalayer.renderLegend(legend) - if (datalayer.options.description) { - description.innerHTML = L.Util.toHTML(datalayer.options.description) - } - }) - datalayer.renderToolbox(headline) - L.DomUtil.add('span', '', headline, `${datalayer.options.name} `) - }) - const creditsContainer = L.DomUtil.create('div', 'credits-container', container), - credits = L.DomUtil.createFieldset(creditsContainer, L._('Credits')) - title = L.DomUtil.add('h5', '', credits, L._('User content credits')) - if (this.options.shortCredit || this.options.longCredit) { - L.DomUtil.add( - 'p', - '', - credits, - L.Util.toHTML(this.options.longCredit || this.options.shortCredit) - ) - } - if (this.options.licence) { - const licence = L.DomUtil.add( - 'p', - '', - credits, - `${L._('Map user content has been published under licence')} ` - ) - L.DomUtil.createLink( - '', - licence, - this.options.licence.name, - this.options.licence.url - ) - } else { - L.DomUtil.add('p', '', credits, L._('No licence has been set')) - } - L.DomUtil.create('hr', '', credits) - title = L.DomUtil.create('h5', '', credits) - title.textContent = L._('Map background credits') - const tilelayerCredit = L.DomUtil.create('p', '', credits), - name = L.DomUtil.create('strong', '', tilelayerCredit), - attribution = L.DomUtil.create('span', '', tilelayerCredit) - name.textContent = `${this.selected_tilelayer.options.name} ` - attribution.innerHTML = this.selected_tilelayer.getAttribution() - L.DomUtil.create('hr', '', credits) - const umapCredit = L.DomUtil.create('p', '', credits), - urls = { - leaflet: 'http://leafletjs.com', - django: 'https://www.djangoproject.com', - umap: 'http://wiki.openstreetmap.org/wiki/UMap', - changelog: 'https://umap-project.readthedocs.io/en/master/changelog/', - version: this.options.umap_version, - } - umapCredit.innerHTML = L._( - ` - Powered by Leaflet and - Django, - glued by uMap project - (version {version}). - `, - urls - ) - const browser = L.DomUtil.create('li', '') - L.DomUtil.create('i', 'umap-icon-16 umap-list', browser) - const labelBrowser = L.DomUtil.create('span', '', browser) - labelBrowser.textContent = labelBrowser.title = L._('Browse data') - L.DomEvent.on(browser, 'click', this.openBrowser, this) - const actions = [browser] - if (this.options.facetKey) { - const filter = L.DomUtil.create('li', '') - L.DomUtil.create('i', 'umap-icon-16 umap-add', filter) - const labelFilter = L.DomUtil.create('span', '', filter) - labelFilter.textContent = labelFilter.title = L._('Facet search') - L.DomEvent.on(filter, 'click', this.openFacet, this) - actions.push(filter) - } - this.ui.openPanel({ data: { html: container }, actions: actions }) - }, - - EXPORT_TYPES: { - geojson: { - formatter: function (map) { - return JSON.stringify(map.toGeoJSON(), null, 2) - }, - ext: '.geojson', - filetype: 'application/json', - }, - gpx: { - formatter: function (map) { - return togpx(map.toGeoJSON()) - }, - ext: '.gpx', - filetype: 'application/gpx+xml', - }, - kml: { - formatter: function (map) { - return tokml(map.toGeoJSON()) - }, - ext: '.kml', - filetype: 'application/vnd.google-earth.kml+xml', - }, - csv: { - formatter: function (map) { - const table = [] - map.eachFeature((feature) => { - const row = feature.toGeoJSON()['properties'], - center = feature.getCenter() - delete row['_umap_options'] - row['Latitude'] = center.lat - row['Longitude'] = center.lng - table.push(row) - }) - return csv2geojson.dsv.csvFormat(table) - }, - ext: '.csv', - filetype: 'text/csv', - }, - }, - - renderEditToolbar: function () { - const container = L.DomUtil.create( - 'div', - 'umap-main-edit-toolbox with-transition dark', - this._controlContainer - ) - const leftContainer = L.DomUtil.create('div', 'umap-left-edit-toolbox', container) - const rightContainer = L.DomUtil.create('div', 'umap-right-edit-toolbox', container) - const logo = L.DomUtil.create('div', 'logo', leftContainer) - L.DomUtil.createLink('', logo, 'uMap', '/', null, L._('Go to the homepage')) - const nameButton = L.DomUtil.createButton( - 'map-name', - leftContainer, - '', - this.edit, - this - ) - L.DomEvent.on( - nameButton, - 'mouseover', - function () { - this.ui.tooltip({ - content: L._('Edit the title of the map'), - anchor: nameButton, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const shareStatusButton = L.DomUtil.createButton( - 'share-status', - leftContainer, - '', - this.permissions.edit, - this.permissions - ) - L.DomEvent.on( - shareStatusButton, - 'mouseover', - function () { - this.ui.tooltip({ - content: L._('Update who can see and edit the map'), - anchor: shareStatusButton, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const update = () => { - const status = this.permissions.getShareStatusDisplay() - nameButton.textContent = this.getDisplayName() - // status is not set until map is saved once - if (status) { - shareStatusButton.textContent = L._('Visibility: {status}', { - status: status, - }) - } - } - update() - this.once('saved', L.bind(update, this)) - if (this.options.editMode === 'advanced') { - L.DomEvent.on(nameButton, 'click', this.edit, this) - L.DomEvent.on(shareStatusButton, 'click', this.permissions.edit, this.permissions) - } - this.on('postsync', L.bind(update, this)) - if (this.options.user) { - L.DomUtil.createLink( - 'umap-user', - rightContainer, - L._(`My Dashboard ({username})`, { - username: this.options.user.name, - }), - this.options.user.url - ) - } - this.help.link(rightContainer, 'edit') - const controlEditCancel = L.DomUtil.createButton( - 'leaflet-control-edit-cancel', - rightContainer, - L.DomUtil.add('span', '', null, L._('Cancel edits')), - this.askForReset, - this - ) - L.DomEvent.on( - controlEditCancel, - 'mouseover', - function () { - this.ui.tooltip({ - content: `${L._('Cancel')} (Ctrl+Z)`, - anchor: controlEditCancel, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const controlEditDisable = L.DomUtil.createButton( - 'leaflet-control-edit-disable', - rightContainer, - L.DomUtil.add('span', '', null, L._('View')), - function (e) { - this.disableEdit(e) - this.ui.closePanel() - }, - this - ) - L.DomEvent.on( - controlEditDisable, - 'mouseover', - function () { - this.ui.tooltip({ - content: `${L._('Back to preview')} (Ctrl+E)`, - anchor: controlEditDisable, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const controlEditSave = L.DomUtil.createButton( - 'leaflet-control-edit-save button', - rightContainer, - L.DomUtil.add('span', '', null, L._('Save')), - this.save, - this - ) - L.DomEvent.on( - controlEditSave, - 'mouseover', - function () { - this.ui.tooltip({ - content: `${L._('Save current edits')} (Ctrl+S)`, - anchor: controlEditSave, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - }, - - renderShareBox: function () { - const container = L.DomUtil.create('div', 'umap-share') - const embedTitle = L.DomUtil.add('h4', '', container, L._('Embed the map')) - const iframe = L.DomUtil.create('textarea', 'umap-share-iframe', container) - const urlTitle = L.DomUtil.add('h4', '', container, L._('Direct link')) - const exportUrl = L.DomUtil.create('input', 'umap-share-url', container) - let option - exportUrl.type = 'text' - const UIFields = [ - ['dimensions.width', { handler: 'Input', label: L._('width') }], - ['dimensions.height', { handler: 'Input', label: L._('height') }], - [ - 'options.includeFullScreenLink', - { handler: 'Switch', label: L._('Include full screen link?') }, - ], - [ - 'options.currentView', - { handler: 'Switch', label: L._('Current view instead of default map view?') }, - ], - [ - 'options.keepCurrentDatalayers', - { handler: 'Switch', label: L._('Keep current visible layers') }, - ], - [ - 'options.viewCurrentFeature', - { handler: 'Switch', label: L._('Open current feature on load') }, - ], - 'queryString.moreControl', - 'queryString.scrollWheelZoom', - 'queryString.miniMap', - 'queryString.scaleControl', - 'queryString.onLoadPanel', - 'queryString.captionBar', - 'queryString.captionMenus', - ] - for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) { - UIFields.push(`queryString.${this.HIDDABLE_CONTROLS[i]}Control`) - } - const iframeExporter = new L.U.IframeExporter(this) - const buildIframeCode = () => { - iframe.innerHTML = iframeExporter.build() - exportUrl.value = window.location.protocol + iframeExporter.buildUrl() - } - buildIframeCode() - const builder = new L.U.FormBuilder(iframeExporter, UIFields, { - callback: buildIframeCode, - }) - const iframeOptions = L.DomUtil.createFieldset(container, L._('Export options')) - iframeOptions.appendChild(builder.build()) - if (this.options.shortUrl) { - L.DomUtil.create('hr', '', container) - L.DomUtil.add('h4', '', container, L._('Short URL')) - const shortUrl = L.DomUtil.create('input', 'umap-short-url', container) - shortUrl.type = 'text' - shortUrl.value = this.options.shortUrl - } - L.DomUtil.create('hr', '', container) - L.DomUtil.add('h4', '', container, L._('Backup data')) - const downloadUrl = L.Util.template(this.options.urls.map_download, { - map_id: this.options.umap_id, - }) - const link = L.DomUtil.createLink( - 'button', - container, - L._('Download full data'), - downloadUrl - ) - let name = this.options.name || 'data' - name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase() - link.setAttribute('download', `${name}.umap`) - L.DomUtil.create('hr', '', container) - L.DomUtil.add('h4', '', container, L._('Download data')) - const typeInput = L.DomUtil.create('select', '', container) - typeInput.name = 'format' - const exportCaveat = L.DomUtil.add( - 'small', - 'help-text', - container, - L._('Only visible features will be downloaded.') - ) - for (const key in this.EXPORT_TYPES) { - if (this.EXPORT_TYPES.hasOwnProperty(key)) { - option = L.DomUtil.create('option', '', typeInput) - option.value = key - option.textContent = this.EXPORT_TYPES[key].name || key - if (this.EXPORT_TYPES[key].selected) option.selected = true - } - } - L.DomUtil.createButton( - 'button', - container, - L._('Download data'), - () => this.download(typeInput.value), - this - ) - this.ui.openPanel({ data: { html: container } }) - }, - -}) - -L.U.TileLayerControl = L.Control.extend({ - options: { - position: 'topleft', - }, - - initialize: function (map, options) { - this.map = map - L.Control.prototype.initialize.call(this, options) - }, - - onAdd: function () { - const container = L.DomUtil.create('div', 'leaflet-control-tilelayers umap-control') - const changeMapBackgroundButton = L.DomUtil.createButton( - '', - container, - L._('Change map background'), - this.openSwitcher, - this - ) - L.DomEvent.on(changeMapBackgroundButton, 'dblclick', L.DomEvent.stopPropagation) - return container - }, - - openSwitcher: function (options) { - this._tilelayers_container = L.DomUtil.create( - 'ul', - 'umap-tilelayer-switcher-container' - ) - this.buildList(options) - }, - - buildList: function (options) { - this.map.eachTileLayer(function (tilelayer) { - if ( - window.location.protocol === 'https:' && - tilelayer.options.url_template.indexOf('http:') === 0 - ) - return - this.addTileLayerElement(tilelayer, options) - }, this) - this.map.ui.openPanel({ - data: { html: this._tilelayers_container }, - className: options.className, - }) - }, - - addTileLayerElement: function (tilelayer, options) { - const selectedClass = this.map.hasLayer(tilelayer) ? 'selected' : '', - el = L.DomUtil.create('li', selectedClass, this._tilelayers_container), - img = L.DomUtil.create('img', '', el), - name = L.DomUtil.create('div', '', el) - img.src = L.Util.template(tilelayer.options.url_template, this.map.demoTileInfos) - img.loading = 'lazy' - name.textContent = tilelayer.options.name - L.DomEvent.on( - el, - 'click', - function () { - this.map.selectTileLayer(tilelayer) - if (options && options.callback) options.callback(tilelayer) - }, - this - ) - }, -}) - -L.U.AttributionControl = L.Control.Attribution.extend({ - options: { - prefix: '', - }, - - _update: function () { - L.Control.Attribution.prototype._update.call(this) - // Use our how container, so we can hide/show on small screens - const credits = this._container.innerHTML - this._container.innerHTML = '' - const container = L.DomUtil.add( - 'div', - 'attribution-container', - this._container, - credits - ) - if (this._map.options.shortCredit) { - L.DomUtil.add( - 'span', - '', - container, - ` — ${L.Util.toHTML(this._map.options.shortCredit)}` - ) - } - if (this._map.options.captionMenus) { - const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`) - L.DomEvent.on(link, 'click', L.DomEvent.stop) - .on(link, 'click', this._map.displayCaption, this._map) - .on(link, 'dblclick', L.DomEvent.stop) - } - if (window.top === window.self && this._map.options.captionMenus) { - // We are not in iframe mode - L.DomUtil.createLink('', container, ` — ${L._('Home')}`, '/') - } - if (this._map.options.captionMenus) { - L.DomUtil.createLink( - '', - container, - ` — ${L._('Powered by uMap')}`, - 'https://github.com/umap-project/umap/' - ) - } - L.DomUtil.createLink('attribution-toggle', this._container, '') - }, -}) - -L.U.StarControl = L.Control.extend({ - options: { - position: 'topleft', - }, - - onAdd: function (map) { - const status = map.options.starred ? ' starred' : '' - const container = L.DomUtil.create( - 'div', - `leaflet-control-star umap-control${status}` - ) - const starMapButton = L.DomUtil.createButton( - '', - container, - L._('Star this map'), - map.star, - map - ) - L.DomEvent.on(starMapButton, 'dblclick', L.DomEvent.stopPropagation) - return container - }, -}) - -L.U.Search = L.PhotonSearch.extend({ - initialize: function (map, input, options) { - L.PhotonSearch.prototype.initialize.call(this, map, input, options) - this.options.url = map.options.urls.search - if (map.options.maxBounds) this.options.bbox = map.options.maxBounds.toBBoxString() - }, - - onBlur: function (e) { - // Overrided because we don't want to hide the results on blur. - this.fire('blur') - }, - - formatResult: function (feature, el) { - const self = this - const tools = L.DomUtil.create('span', 'search-result-tools', el), - zoom = L.DomUtil.create('i', 'feature-zoom_to', tools), - edit = L.DomUtil.create('i', 'feature-edit show-on-edit', tools) - zoom.title = L._('Zoom to this place') - edit.title = L._('Save this location as new feature') - // We need to use "mousedown" because Leaflet.Photon listen to mousedown - // on el. - L.DomEvent.on(zoom, 'mousedown', (e) => { - L.DomEvent.stop(e) - self.zoomToFeature(feature) - }) - L.DomEvent.on(edit, 'mousedown', (e) => { - L.DomEvent.stop(e) - const datalayer = self.map.defaultEditDataLayer() - const layer = datalayer.geojsonToFeatures(feature) - layer.isDirty = true - layer.edit() - }) - this._formatResult(feature, el) - }, - - zoomToFeature: function (feature) { - const zoom = Math.max(this.map.getZoom(), 16) // Never unzoom. - this.map.setView( - [feature.geometry.coordinates[1], feature.geometry.coordinates[0]], - zoom - ) - }, - - onSelected: function (feature) { - this.zoomToFeature(feature) - this.map.ui.closePanel() - }, -}) - -L.U.SearchControl = L.Control.extend({ - options: { - position: 'topleft', - }, - - onAdd: function (map) { - const container = L.DomUtil.create('div', 'leaflet-control-search umap-control') - L.DomEvent.disableClickPropagation(container) - L.DomUtil.createButton( - '', - container, - L._('Search a place name'), - (e) => { - L.DomEvent.stop(e) - this.openPanel(map) - }, - this - ) - return container - }, - - openPanel: function (map) { - const options = { - limit: 10, - noResultLabel: L._('No results'), - } - if (map.options.photonUrl) options.url = map.options.photonUrl - const container = L.DomUtil.create('div', '') - - const title = L.DomUtil.create('h3', '', container) - title.textContent = L._('Search location') - const input = L.DomUtil.create('input', 'photon-input', container) - const resultsContainer = L.DomUtil.create('div', 'photon-autocomplete', container) - this.search = new L.U.Search(map, input, options) - const id = Math.random() - this.search.on('ajax:send', () => { - map.fire('dataloading', { id: id }) - }) - this.search.on('ajax:return', () => { - map.fire('dataload', { id: id }) - }) - this.search.resultsContainer = resultsContainer - map.ui.once('panel:ready', () => { - input.focus() - }) - map.ui.openPanel({ data: { html: container } }) - }, -}) - -L.Control.MiniMap.include({ - initialize: function (layer, options) { - L.Util.setOptions(this, options) - this._layer = this._cloneLayer(layer) - }, - - onMainMapBaseLayerChange: function (e) { - const layer = this._cloneLayer(e.layer) - if (this._miniMap.hasLayer(this._layer)) { - this._miniMap.removeLayer(this._layer) - } - this._layer = layer - this._miniMap.addLayer(this._layer) - }, - - _cloneLayer: function (layer) { - return new L.TileLayer(layer._url, L.Util.extend({}, layer.options)) - }, -}) - -L.Control.Loading.include({ - onAdd: function (map) { - this._container = L.DomUtil.create('div', 'umap-loader', map._controlContainer) - map.on('baselayerchange', this._layerAdd, this) - this._addMapListeners(map) - this._map = map - }, - - _showIndicator: function () { - L.DomUtil.addClass(this._map._container, 'umap-loading') - }, - - _hideIndicator: function () { - L.DomUtil.removeClass(this._map._container, 'umap-loading') - }, -}) - -/* - * Make it dynamic - */ -L.U.ContextMenu = L.Map.ContextMenu.extend({ - _createItems: function (e) { - this._map.setContextMenuItems(e) - L.Map.ContextMenu.prototype._createItems.call(this) - }, - - _showAtPoint: function (pt, e) { - this._items = [] - this._container.innerHTML = '' - this._createItems(e) - L.Map.ContextMenu.prototype._showAtPoint.call(this, pt, e) - }, -}) - -L.U.IframeExporter = L.Evented.extend({ - options: { - includeFullScreenLink: true, - currentView: false, - keepCurrentDatalayers: false, - viewCurrentFeature: false, - }, - - queryString: { - scaleControl: false, - miniMap: false, - scrollWheelZoom: false, - zoomControl: true, - editMode: 'disabled', - moreControl: true, - searchControl: null, - tilelayersControl: null, - embedControl: null, - datalayersControl: true, - onLoadPanel: 'none', - captionBar: false, - captionMenus: true, - }, - - dimensions: { - width: '100%', - height: '300px', - }, - - initialize: function (map) { - this.map = map - this.baseUrl = L.Util.getBaseUrl() - // Use map default, not generic default - this.queryString.onLoadPanel = this.map.options.onLoadPanel - }, - - getMap: function () { - return this.map - }, - - buildUrl: function (options) { - const datalayers = [] - if (this.options.viewCurrentFeature && this.map.currentFeature) { - this.queryString.feature = this.map.currentFeature.getSlug() - } - if (this.options.keepCurrentDatalayers) { - this.map.eachDataLayer((datalayer) => { - if (datalayer.isVisible() && datalayer.umap_id) { - datalayers.push(datalayer.umap_id) - } - }) - this.queryString.datalayers = datalayers.join(',') - } else { - delete this.queryString.datalayers - } - const currentView = this.options.currentView ? window.location.hash : '' - const queryString = L.extend({}, this.queryString, options) - return `${this.baseUrl}?${L.Util.buildQueryString(queryString)}${currentView}` - }, - - build: function () { - const iframeUrl = this.buildUrl() - let code = `` - if (this.options.includeFullScreenLink) { - const fullUrl = this.buildUrl({ scrollWheelZoom: true }) - code += `` - } - return code - }, -}) - -L.U.Editable = L.Editable.extend({ - initialize: function (map, options) { - L.Editable.prototype.initialize.call(this, map, options) - this.on( - 'editable:drawing:start editable:drawing:click editable:drawing:move', - this.drawingTooltip - ) - this.on('editable:drawing:end', this.closeTooltip) - // Layer for items added by users - this.on('editable:drawing:cancel', (e) => { - if (e.layer._latlngs && e.layer._latlngs.length < e.layer.editor.MIN_VERTEX) - e.layer.del() - if (e.layer instanceof L.U.Marker) e.layer.del() - }) - this.on('editable:drawing:commit', function (e) { - e.layer.isDirty = true - if (this.map.editedFeature !== e.layer) e.layer.edit(e) - }) - this.on('editable:editing', (e) => { - const layer = e.layer - layer.isDirty = true - if (layer._tooltip && layer.isTooltipOpen()) { - layer._tooltip.setLatLng(layer.getCenter()) - layer._tooltip.update() - } - }) - this.on('editable:vertex:ctrlclick', (e) => { - const index = e.vertex.getIndex() - if (index === 0 || (index === e.vertex.getLastIndex() && e.vertex.continue)) - e.vertex.continue() - }) - this.on('editable:vertex:altclick', (e) => { - if (e.vertex.editor.vertexCanBeDeleted(e.vertex)) e.vertex.delete() - }) - this.on('editable:vertex:rawclick', this.onVertexRawClick) - }, - - createPolyline: function (latlngs) { - return new L.U.Polyline(this.map, latlngs, this._getDefaultProperties()) - }, - - createPolygon: function (latlngs) { - return new L.U.Polygon(this.map, latlngs, this._getDefaultProperties()) - }, - - createMarker: function (latlng) { - return new L.U.Marker(this.map, latlng, this._getDefaultProperties()) - }, - - _getDefaultProperties: function () { - const result = {} - if (this.map.options.featuresHaveOwner && this.map.options.hasOwnProperty('user')) { - result.geojson = { properties: { owner: this.map.options.user.id } } - } - return result - }, - - connectCreatedToMap: function (layer) { - // Overrided from Leaflet.Editable - const datalayer = this.map.defaultEditDataLayer() - datalayer.addLayer(layer) - layer.isDirty = true - return layer - }, - - drawingTooltip: function (e) { - if (e.layer instanceof L.Marker && e.type == 'editable:drawing:start') { - this.map.ui.tooltip({ content: L._('Click to add a marker') }) - } - if (!(e.layer instanceof L.Polyline)) { - // only continue with Polylines and Polygons - return - } - - let content = L._('Drawing') - let measure - if (e.layer.editor._drawnLatLngs) { - // when drawing (a Polyline or Polygon) - if (!e.layer.editor._drawnLatLngs.length) { - // when drawing first point - if (e.layer instanceof L.Polygon) { - content = L._('Click to start drawing a polygon') - } else if (e.layer instanceof L.Polyline) { - content = L._('Click to start drawing a line') - } - } else { - const tmpLatLngs = e.layer.editor._drawnLatLngs.slice() - tmpLatLngs.push(e.latlng) - measure = e.layer.getMeasure(tmpLatLngs) - - if (e.layer.editor._drawnLatLngs.length < e.layer.editor.MIN_VERTEX) { - // when drawing second point - content = L._('Click to continue drawing') - } else { - // when drawing third point (or more) - content = L._('Click last point to finish shape') - } - } - } else { - // when moving an existing point - measure = e.layer.getMeasure() - } - if (measure) { - if (e.layer instanceof L.Polygon) { - content += L._(' (area: {measure})', { measure: measure }) - } else if (e.layer instanceof L.Polyline) { - content += L._(' (length: {measure})', { measure: measure }) - } - } - if (content) { - this.map.ui.tooltip({ content: content }) - } - }, - - closeTooltip: function () { - this.map.ui.closeTooltip() - }, - - onVertexRawClick: function (e) { - e.layer.onVertexRawClick(e) - L.DomEvent.stop(e) - e.cancel() - }, -}) diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 6a5b29af..b914df11 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -39,7 +39,7 @@ - +