diff --git a/dist/index.html b/dist/index.html index 8690013..455e7b6 100644 --- a/dist/index.html +++ b/dist/index.html @@ -201,11 +201,12 @@
Select/highlight tree
const parameters = getParameters(); map = new greenstand.Map({ onLoad: () => console.log("onload"), - onClickTree: () => console.log("onClickTree"), onFindNearestAt: () => console.log("onFindNearstAt"), onError: () => console.log("onError"), }); map.on(greenstand.Map.REGISTERED_EVENTS.MOVE_END, handleMoveEnd); + map.on(greenstand.Map.REGISTERED_EVENTS.TREE_SELECTED, (data) => console.log(data)); + map.on(greenstand.Map.REGISTERED_EVENTS.MULTIPLE_TREES_SELECTED, (data) => console.log(data)); map.mount(document.getElementById("map")); map.setFilters(parameters); }; diff --git a/package-lock.json b/package-lock.json index 24bdddb..7c95d3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "treetracker-web-map-core", - "version": "2.6.0", + "version": "2.7.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "treetracker-web-map-core", - "version": "2.6.0", + "version": "2.7.2", "license": "ISC", "dependencies": { "axios": "^0.24.0", @@ -14,6 +14,7 @@ "events": "^3.3.0", "expect-runtime": "^0.10.1", "leaflet": "^1.7.1", + "leaflet-draw": "^1.0.4", "leaflet-utfgrid": "git+https://github.com/dadiorchen/Leaflet.UTFGrid.git", "leaflet.gridlayer.googlemutant": "^0.12.1", "lodash": "^4.17.21", @@ -9137,6 +9138,11 @@ "resolved": "https://registry.npm.taobao.org/leaflet/download/leaflet-1.7.1.tgz", "integrity": "sha1-ENaEkW7f4b9B1oijuXEnwDIqKhk=" }, + "node_modules/leaflet-draw": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==" + }, "node_modules/leaflet-utfgrid": { "version": "0.3.0", "resolved": "git+ssh://git@github.com/dadiorchen/Leaflet.UTFGrid.git#2bdd74c2ca5298bcc820bc9e0deeb1ae69556526", @@ -20332,6 +20338,11 @@ "resolved": "https://registry.npm.taobao.org/leaflet/download/leaflet-1.7.1.tgz", "integrity": "sha1-ENaEkW7f4b9B1oijuXEnwDIqKhk=" }, + "leaflet-draw": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==" + }, "leaflet-utfgrid": { "version": "git+ssh://git@github.com/dadiorchen/Leaflet.UTFGrid.git#2bdd74c2ca5298bcc820bc9e0deeb1ae69556526", "from": "leaflet-utfgrid@git+https://github.com/dadiorchen/Leaflet.UTFGrid.git", diff --git a/package.json b/package.json index f826e61..3540c2d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "events": "^3.3.0", "expect-runtime": "^0.10.1", "leaflet": "^1.7.1", + "leaflet-draw": "^1.0.4", "leaflet-utfgrid": "git+https://github.com/dadiorchen/Leaflet.UTFGrid.git", "leaflet.gridlayer.googlemutant": "^0.12.1", "lodash": "^4.17.21", diff --git a/src/Alert.js b/src/Alert.js index c79cdb6..d5f1e0f 100644 --- a/src/Alert.js +++ b/src/Alert.js @@ -2,7 +2,9 @@ import './style.css' export default class Alert { - constructor() {} + constructor() { + this.id = 0 + } mount(element) { // create a div and mount to the element @@ -26,11 +28,15 @@ export default class Alert { element.appendChild(this.alert) } - show(message) { + show(message, time) { + clearTimeout(this.id) document.getElementsByClassName( 'greenstand-alert-message-box', )[0].innerHTML = message this.alert.style.display = 'block' + if (time) { + this.id = setTimeout(() => this.hide(), time) + } } hide() { diff --git a/src/DrawTool.js b/src/DrawTool.js new file mode 100644 index 0000000..14ec08d --- /dev/null +++ b/src/DrawTool.js @@ -0,0 +1,88 @@ +import 'leaflet-draw' +import 'leaflet-draw/dist/leaflet.draw.css' + +class DrawTool { + constructor(map) { + this.map = map + this.isDrawingMode = false + this._loadEditor() + this.onSelecetMultTree = null + } + onSelecetMultiplePoints(funct) { + this.onSelecetMultPoints = funct + } + async _loadEditor() { + // FeatureGroup is to store editable layers + var drawnItems = new window.L.FeatureGroup() + this.map.addLayer(drawnItems) + var drawControl = new window.L.Control.Draw({ + draw: { + marker: false, + polyline: false, + circlemarker: false, + circle: false, + polygon: { + allowIntersection: false, + }, + }, + edit: { + featureGroup: drawnItems, + poly: { + allowIntersection: false, + }, + }, + }) + var editOnlyControl = new window.L.Control.Draw({ + draw: false, + edit: { + featureGroup: drawnItems, + poly: { + allowIntersection: false, + }, + }, + }) + this.map.addControl(drawControl) + + this.map.on('draw:created', async (e) => { + let layer = e.layer + let points = layer._latlngs[0] + drawnItems.addLayer(layer) + //disable draw tool + this.map.removeControl(drawControl) + this.map.addControl(editOnlyControl) + if (this.onSelecetMultPoints) { + this.onSelecetMultPoints(points) + } + }) + this.map.on('draw:edited', async (e) => { + let layers = e.layers._layers + let polygon = Object.values(layers)[0] + if (polygon) { + if (this.onSelecetMultPoints) { + this.onSelecetMultPoints(polygon._latlngs[0]) + } + } + }) + this.map.on('draw:deleted', async (e) => { + //enable draw tool if all polygons deleted + if (drawnItems.getLayers().length == 0) { + this.map.removeControl(editOnlyControl) + this.map.addControl(drawControl) + } + }) + //enable drawing mode which prevent user move when clicking on icon tree or group + this.map.on('draw:drawstart ', (e) => { + this.isDrawingMode = true + }) + this.map.on('draw:drawstop ', (e) => { + this.isDrawingMode = false + }) + this.map.on('draw:editstart ', (e) => { + this.isDrawingMode = true + }) + this.map.on('draw:editstop ', (e) => { + this.isDrawingMode = false + }) + } +} +export default DrawTool diff --git a/src/Map.js b/src/Map.js index b600e5e..ef7fa05 100644 --- a/src/Map.js +++ b/src/Map.js @@ -7,7 +7,9 @@ import expect from 'expect-runtime' import log from 'loglevel' import _ from 'lodash' import 'leaflet' +import 'leaflet-draw' import 'leaflet/dist/leaflet.css' +import 'leaflet-draw/dist/leaflet.draw.css' import 'leaflet-utfgrid/L.UTFGrid' import 'leaflet.gridlayer.googlemutant' @@ -22,6 +24,7 @@ import Alert from './Alert' import TileLoadingMonitor from './TileLoadingMonitor' import ButtonPanel from './ButtonPanel' import NearestTreeArrows from './NearestTreeArrows' +import DrawTool from './DrawTool' class MapError extends Error {} @@ -31,8 +34,14 @@ export default class Map { // events static REGISTERED_EVENTS = { TREE_SELECTED: 'tree-selected', + MULTIPLE_TREES_SELECTED: 'multiple-trees-selected', TREE_UNSELECTED: 'tree-unselected', MOVE_END: 'move-end', + LOAD: 'load', + TREE_CLICKED: 'tree-clicked', + //not implemented this event yet + // FIND_NEAREST: 'find-nearest', + ERROR: 'error', } constructor(options) { @@ -70,6 +79,19 @@ export default class Map { this._mountDomElement = null log.warn('map core version:', require('../package.json').version) + + // Deprecation warnings + let deprecatedMethods = [ + 'onLoad', + 'onClickTree', + 'onFindNearestAt', + 'onError', + ] + deprecatedMethods.forEach((method) => { + if (this[method]) { + log.warn(`${method} is deprecated. Use map.on() instead.`) + } + }) } /** *************************** static *************************** */ @@ -306,7 +328,7 @@ export default class Map { ) this.layerUtfGrid.on('click', (e) => { log.warn('click:', e) - if (e.data) { + if (e.data && !this.drawTool.isDrawingMode) { this._clickMarker(Map._parseUtfData(e.data)) } }) @@ -518,6 +540,9 @@ export default class Map { if (this.onClickTree) { this.onClickTree(data) } + if (this.events.listenerCount(Map.REGISTERED_EVENTS.TREE_CLICKED) > 0) { + this.events.emit(Map.REGISTERED_EVENTS.TREE_CLICKED, data) + } } else if (data.type === 'cluster') { if (data.zoom_to) { log.info('found zoom to:', data.zoom_to) @@ -970,6 +995,37 @@ export default class Map { : this.nearestTreeArrow.showArrow(placement) } + async _getTreesFromPoly(poly) { + try { + let polypoints = poly.map(({ lat, lng }) => { + return { + lat, + lon: lng, + } + }) + //add another first point to make polygon enclosed + polypoints.push({ lat: poly[0].lat, lon: poly[0].lng }) + this.alert.show('Collecting trees data') + const result = await this.requester.request({ + url: `${this.queryApiServerUrl}/gis`, + data: { + polygon: polypoints, + }, + headers: { 'Content-Type': 'application/json' }, + method: 'post', + }) + this.alert.hide() + return result + } catch (err) { + this.alert.show( + 'Can not collecting tree data, please try again later', + 5000, + ) + console.info(err.message || 'Unknown') + return null + } + } + async _moveToNearestTree() { const nearest = await this._getNearest() if (nearest) { @@ -1307,7 +1363,9 @@ export default class Map { if (this.onLoad) { this.onLoad() } - + if (this.events.listenerCount(Map.REGISTERED_EVENTS.LOAD) > 0) { + this.events.emit(Map.REGISTERED_EVENTS.LOAD) + } if (this.debug) { await this._loadDebugLayer() } @@ -1318,15 +1376,61 @@ export default class Map { if (this.onError) { this.onError(e) } + if (this.events.listenerCount(Map.REGISTERED_EVENTS.ERROR) > 0) { + this.events.emit(Map.REGISTERED_EVENTS.ERROR, e) + } } } } on(eventName, handler) { + const isValidEvent = Object.values(Map.REGISTERED_EVENTS).includes( + eventName, + ) + if (!isValidEvent) { + log.error('Invalid event name:', eventName) + return + } //TODO check event name enum if (handler) { log.info('register event:', eventName) this.events.on(eventName, handler) + } else { + log.error('No handler provided for event:', eventName) + } + } + + off(eventName, handler) { + const isValidEvent = Object.values(Map.REGISTERED_EVENTS).includes( + eventName, + ) + if (!isValidEvent) { + log.error('Invalid event name:', eventName) + return + } + + if (handler) { + log.info('remove event:', eventName) + this.events.off(eventName, handler) + } else { + log.error('No handler provided for event removal:', eventName) + } + } + + once(eventName, handler) { + const isValidEvent = Object.values(Map.REGISTERED_EVENTS).includes( + eventName, + ) + if (!isValidEvent) { + log.error('Invalid event name:', eventName) + return + } + + if (handler) { + log.info('register one-time event:', eventName) + this.events.once(eventName, handler) + } else { + log.error('No handler provided for this one-time event:', eventName) } } @@ -1349,7 +1453,46 @@ export default class Map { await this._unselectMarker() await this._unloadTileServer() await this._loadTileServer() + await this._loadEditor() + } + } + + async _loadEditor() { + //create draw tool + this.drawTool = new DrawTool(this.map) + + //add panel to track how many selected + var panel = window.L.control({ position: 'topright' }) + panel.onAdd = function (map) { + var div = window.L.DomUtil.create('div', 'info') + div.innerHTML = ` +
+

Tree Count: 0

+
` + return div + } + panel.addTo(this.map) + + //create event for select multiple trees event + const onSelectMultTrees = async (points) => { + const result = await this._getTreesFromPoly(points) + var total = result?.trees?.length || 0 + document.getElementById('treeTotal').innerHTML = total + if ( + this.events.listenerCount( + Map.REGISTERED_EVENTS.MULTIPLE_TREES_SELECTED, + ) > 0 + ) { + this.events.emit( + Map.REGISTERED_EVENTS.MULTIPLE_TREES_SELECTED, + result?.trees || [], + ) + } } + //add select multiple trees event to the draw tool + this.drawTool.onSelecetMultiplePoints(onSelectMultTrees) } clearSelection() { diff --git a/src/Map.test.mjs b/src/Map.test.mjs index 532cc5e..bfc9791 100644 --- a/src/Map.test.mjs +++ b/src/Map.test.mjs @@ -45,4 +45,28 @@ describe('Map', () => { zoomLevel: 3, }) }) + + // it("Test trigger events",async()=>{ + // const request = jest.fn(() => response) + // Requester.mockImplementation(() => ({ + // request, + // })) + // const event_check = { + // tree_selected:0, + // } + // const map = new Map({ + // userid: '1', + // width: 1440, + // height: 510, + // moreEffect: false, + // filters: { + // wallet: 'mayeda', + // }, + // //function to test error event + // _mountComponents: ()=>{ + // throw new Error("Error event"); + // } + // }) + // map.on(map.REGISTERED_EVENTS.TREE_SELECTED) + // }) })