From 50986f06b7d7c0c35ddef55b6fde8589b712378f Mon Sep 17 00:00:00 2001 From: ztimson Date: Fri, 30 Aug 2019 23:30:15 -0400 Subject: [PATCH] Syncing drawings! --- src/app/models/mapSymbol.ts | 2 + src/app/services/map.service.ts | 118 +++++++++++----------- src/app/services/sync.service.ts | 10 +- src/app/views/map/map.component.ts | 154 ++++++++++++++++++++--------- 4 files changed, 177 insertions(+), 107 deletions(-) diff --git a/src/app/models/mapSymbol.ts b/src/app/models/mapSymbol.ts index 416f78b..c5a9715 100644 --- a/src/app/models/mapSymbol.ts +++ b/src/app/models/mapSymbol.ts @@ -7,6 +7,7 @@ export interface MapData { circles?: Circle[]; markers?: Marker[]; measurements?: Measurement[]; + polylines?: Polyline[]; rectangles?: Rectangle[]; } @@ -35,6 +36,7 @@ export interface Rectangle extends MapSymbol { export interface Polyline extends MapSymbol { latlng: LatLng[]; + weight?: number; } export interface Marker extends MapSymbol { diff --git a/src/app/services/map.service.ts b/src/app/services/map.service.ts index f6d0210..92792df 100644 --- a/src/app/services/map.service.ts +++ b/src/app/services/map.service.ts @@ -1,7 +1,7 @@ import {BehaviorSubject} from "rxjs"; import {latLngDistance} from "../utils"; import {environment} from "../../environments/environment"; -import {Circle, LatLng, MapSymbol, Marker, Measurement, Rectangle} from "../models/mapSymbol"; +import {Circle, LatLng, MapSymbol, Marker, Measurement, Polyline, Rectangle} from "../models/mapSymbol"; declare const L; @@ -24,22 +24,26 @@ const MARKER = L.icon({iconUrl: '/assets/images/marker.png', iconSize: [40, 55], const MEASURE = L.icon({iconUrl: '/assets/images/measure.png', iconSize: [75, 50], iconAnchor: [25, 25]}); export class MapService { + private readonly map; + private circles = []; private drawListener; private markers = []; private measurements = []; private mapLayer; + private polyline = []; private rectangles = []; private weatherLayer; click = new BehaviorSubject<{latlng: LatLng, symbol?: MapSymbol, item?: any}>(null); - drawingColor = '#ff4141'; - drawingWeight = 10; - map; + touch = new BehaviorSubject<{type: string, latlng: LatLng}>(null); constructor(private elementId: string) { this.map = L.map(elementId, {attributionControl: false, editable: true, tap: true, zoomControl: false, maxBoundsViscosity: 1, doubleClickZoom: false}).setView({lat: 0, lng: 0}, 10); this.map.on('click', (e) => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}})); + this.map.on('touchstart', (e) => this.touch.next({type: 'start', latlng: {lat: e.latlng.lat, lng: e.latlng.lng}})); + this.map.on('touchmove', (e) => this.touch.next({type: 'move', latlng: {lat: e.latlng.lat, lng: e.latlng.lng}})); + this.map.on('touchend', (e) => this.touch.next({type: 'end', latlng: {lat: e.latlng.lat, lng: e.latlng.lng}})); this.setMapLayer(); } @@ -64,6 +68,7 @@ export class MapService { this.circles = this.circles.filter(c => c != c); this.markers = this.markers.filter(m => m != s); this.measurements = this.measurements.filter(m => m != s); + this.polyline = this.polyline.filter(p => p != s); this.rectangles = this.rectangles.filter(r => r != s); }); } @@ -72,6 +77,7 @@ export class MapService { this.circles.forEach(c => this.delete(c)); this.markers.forEach(m => this.delete(m)); this.measurements.forEach(m => this.delete(m)); + this.polyline.forEach(p => this.delete(p)); this.rectangles.forEach(r => this.delete(r)); } @@ -89,6 +95,52 @@ export class MapService { } } + newCircle(c: Circle) { + let circle = L.circle(c.latlng, Object.assign({}, c)).addTo(this.map); + if(c.label) circle.bindTooltip(c.label, {permanent: true, direction: 'center'}); + circle.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: c, item: circle})); + if(!c.noDelete) this.circles.push(circle); + return circle; + } + + newMarker(m: Marker) { + let marker = L.marker(m.latlng, Object.assign({}, m, {icon: m.icon ? this.getIcon(m.icon) : MARKER})).addTo(this.map); + if(m.label) marker.bindTooltip(m.label, {permanent: true, direction: 'bottom'}); + marker.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: m, item: marker})); + if(!m.noDelete) this.markers.push(marker); + return marker; + } + + newMeasurement(m: Measurement) { + let line = L.polyline([m.latlng, m.latlng2], Object.assign({}, m)); + let decoration = L.polylineDecorator(line, {patterns: [ + {offset: '100%', repeat: 0, symbol: L.Symbol.arrowHead({pixelSize: 10, polygon: false, headAngle: 180, pathOptions: m})}, + {offset: '-100%', repeat: 0, symbol: L.Symbol.arrowHead({pixelSize: 10, polygon: false, headAngle: 180, pathOptions: m})} + ]}); + let group = new L.LayerGroup([line, decoration]).addTo(this.map); + if(!m.noDelete) this.measurements.push(group); + + let distance = latLngDistance(m.latlng, m.latlng2); + line.bindPopup(`${distance > 1000 ? Math.round(distance / 100) / 10 : Math.round(distance)} ${distance > 1000 ? 'k' : ''}m`, {autoClose: false, closeOnClick: false}).openPopup(); + line.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: m, item: group})); + return group; + } + + newPolyline(p: Polyline) { + let polyline = new L.Polyline(p.latlng, Object.assign({}, p)).addTo(this.map); + polyline.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: p, item: polyline})); + if(!p.noDelete) this.polyline.push(polyline); + return polyline; + } + + newRectangle(r: Rectangle) { + let rect = new L.Rectangle([r.latlng, r.latlng2], Object.assign({}, r)).addTo(this.map); + if(r.label) rect.bindTooltip(r.label, {permanent: true, direction: 'center'}); + rect.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: r, item: rect})); + if(!r.noDelete) this.rectangles.push(rect); + return rect; + } + setMapLayer(layer?: MapLayers) { if(this.mapLayer) this.map.removeLayer(this.mapLayer); if(layer == null) layer = MapLayers.ESRI_IMAGERY; @@ -131,62 +183,4 @@ export class MapService { } if(this.weatherLayer) this.weatherLayer.layer.addTo(this.map); } - - newCircle(c: Circle) { - let circle = L.circle(c.latlng, Object.assign({}, c)).addTo(this.map); - if(c.label) circle.bindTooltip(c.label, {permanent: true, direction: 'center'}); - circle.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: c, item: circle})); - if(!c.noDelete) this.circles.push(circle); - return circle; - } - - newMarker(m: Marker) { - let marker = L.marker(m.latlng, Object.assign({}, m, {icon: m.icon ? this.getIcon(m.icon) : MARKER})).addTo(this.map); - if(m.label) marker.bindTooltip(m.label, {permanent: true, direction: 'bottom'}); - marker.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: m, item: marker})); - if(!m.noDelete) this.markers.push(marker); - return marker; - } - - newMeasurement(m: Measurement) { - let line = L.polyline([m.latlng, m.latlng2], Object.assign({}, m)); - let decoration = L.polylineDecorator(line, {patterns: [ - {offset: '100%', repeat: 0, symbol: L.Symbol.arrowHead({pixelSize: 10, polygon: false, headAngle: 180, pathOptions: m})}, - {offset: '-100%', repeat: 0, symbol: L.Symbol.arrowHead({pixelSize: 10, polygon: false, headAngle: 180, pathOptions: m})} - ]}); - let group = new L.LayerGroup([line, decoration]).addTo(this.map); - if(!m.noDelete) this.measurements.push(group); - - let distance = latLngDistance(m.latlng, m.latlng2); - line.bindPopup(`${distance > 1000 ? Math.round(distance / 100) / 10 : Math.round(distance)} ${distance > 1000 ? 'k' : ''}m`, {autoClose: false, closeOnClick: false}).openPopup(); - line.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: m, item: group})); - return group; - } - - newRectangle(r: Rectangle) { - let rect = new L.Rectangle([r.latlng, r.latlng2], Object.assign({}, r)).addTo(this.map); - if(r.label) rect.bindTooltip(r.label, {permanent: true, direction: 'center'}); - rect.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, symbol: r, item: rect})); - if(!r.noDelete) this.rectangles.push(rect); - return rect; - } - - startDrawing() { - this.lock(); - this.drawListener = e => { - let poly = L.polyline([e.latlng], {interactive: true, color: this.drawingColor, weight: this.drawingWeight}).addTo(this.map); - poly.on('click', e => this.click.next({latlng: {lat: e.latlng.lat, lng: e.latlng.lng}, item: poly})); - let pushLine = e => poly.addLatLng(e.latlng); - this.map.on('touchmove', pushLine); - this.map.on('touchend', () => this.map.off('touchmove', pushLine)); - }; - - this.map.on('touchstart', this.drawListener); - } - - stopDrawing() { - this.lock(true); - this.map.setMaxBounds(null); - this.map.off('touchstart', this.drawListener); - } } diff --git a/src/app/services/sync.service.ts b/src/app/services/sync.service.ts index a0c3805..ade556d 100644 --- a/src/app/services/sync.service.ts +++ b/src/app/services/sync.service.ts @@ -1,7 +1,7 @@ import {Injectable} from "@angular/core"; import {AngularFirestore, AngularFirestoreCollection} from "@angular/fire/firestore"; import {BehaviorSubject, Subscription} from "rxjs"; -import {Circle, MapData, Marker, Measurement, Rectangle} from "../models/mapSymbol"; +import {Circle, MapData, Marker, Measurement, Polyline, Rectangle} from "../models/mapSymbol"; import * as _ from 'lodash'; @Injectable({ @@ -43,6 +43,13 @@ export class SyncService { this.save(); } + addPolyline(polyline: Polyline) { + let map = this.mapSymbols.value; + if(!map.polylines) map.polylines = []; + map.polylines.push(polyline); + this.save(); + } + addRectangle(rect: Rectangle) { let map = this.mapSymbols.value; if(!map.rectangles) map.rectangles = []; @@ -55,6 +62,7 @@ export class SyncService { if(map.circles) symbols.forEach(s => map.circles = map.circles.filter(c => !_.isEqual(s, c))); if(map.markers) symbols.forEach(s => map.markers = map.markers.filter(m => !_.isEqual(s, m))); if(map.measurements) symbols.forEach(s => map.measurements = map.measurements.filter(m => !_.isEqual(s, m))); + if(map.polylines) symbols.forEach(s => map.polylines = map.polylines.filter(p => !_.isEqual(s, p))); if(map.rectangles) symbols.forEach(s => map.rectangles = map.rectangles.filter(r => !_.isEqual(s, r))); this.save(); } diff --git a/src/app/views/map/map.component.ts b/src/app/views/map/map.component.ts index 1169e5d..dbbb447 100644 --- a/src/app/views/map/map.component.ts +++ b/src/app/views/map/map.component.ts @@ -40,7 +40,11 @@ export class MapComponent implements OnInit { startCircle = menuItem => { this.sub = this.map.click.pipe(skip(1), take(1)).subscribe(async e => { - let dimensions = await this.dialog.open(DimensionsDialogComponent, {data: ['Radius (m)'], disableClose: true, panelClass: 'pb-0'}).afterClosed().toPromise(); + let dimensions = await this.dialog.open(DimensionsDialogComponent, { + data: ['Radius (m)'], + disableClose: true, + panelClass: 'pb-0' + }).afterClosed().toPromise(); menuItem.enabled = false; let circle = {latlng: e.latlng, radius: dimensions[0] || 500, color: '#ff4141'}; this.syncService.addCircle(circle); @@ -49,11 +53,31 @@ export class MapComponent implements OnInit { startDelete = () => { this.sub = this.map.click.pipe(skip(1), filter(e => !!e.symbol)).subscribe(e => { - if(!!e.symbol && e.symbol.noDelete) return; + if (!!e.symbol && e.symbol.noDelete) return; this.syncService.delete(e.symbol) }); }; + startDrawing = () => { + this.showPalette = true; + this.map.lock(); + this.sub = this.map.touch.pipe(skip(1), filter(e => e.type == 'start'), finalize(() => { + this.showPalette = false; + this.map.lock(true); + })).subscribe(e => { + let p = {latlng: [e.latlng], noDelete: true, color: this.drawColor, weight: 8}; + let polyline = this.map.newPolyline(p); + let drawingSub = this.map.touch.pipe(filter(e => e.type == 'move')).subscribe(e => polyline.addLatLng(e.latlng)); + this.map.touch.pipe(filter(e => e.type == 'end'), take(1)).subscribe(() => { + drawingSub.unsubscribe(); + p.noDelete = false; + p.latlng = polyline.getLatLngs().map(latlng => ({lat: latlng.lat, lng: latlng.lng})); + this.map.delete(polyline); + this.syncService.addPolyline(p); + }); + }) + }; + startMarker = menuItem => { this.sub = this.map.click.pipe(skip(1), take(1)).subscribe(e => { menuItem.enabled = false; @@ -65,9 +89,14 @@ export class MapComponent implements OnInit { startMeasuring = menuItem => { let lastPoint; this.sub = this.map.click.pipe(skip(1), take(2), finalize(() => this.map.delete(lastPoint))).subscribe(e => { - if(lastPoint) { + if (lastPoint) { menuItem.enabled = false; - let measurement = {latlng: {lat: lastPoint.getLatLng().lat, lng: lastPoint.getLatLng().lng}, latlng2: e.latlng, color: '#ff4141', weight: 8}; + let measurement = { + latlng: {lat: lastPoint.getLatLng().lat, lng: lastPoint.getLatLng().lng}, + latlng2: e.latlng, + color: '#ff4141', + weight: 8 + }; this.syncService.addMeasurement(measurement); return this.map.delete(lastPoint); } @@ -78,9 +107,13 @@ export class MapComponent implements OnInit { startRectangle = menuItem => { let lastPoint; this.sub = this.map.click.pipe(skip(1), take(2), finalize(() => this.map.delete(lastPoint))).subscribe(e => { - if(lastPoint) { + if (lastPoint) { menuItem.enabled = false; - let rect = {latlng: {lat: lastPoint.getLatLng().lat, lng: lastPoint.getLatLng().lng}, latlng2: e.latlng, color: '#ff4141'}; + let rect = { + latlng: {lat: lastPoint.getLatLng().lat, lng: lastPoint.getLatLng().lng}, + latlng2: e.latlng, + color: '#ff4141' + }; this.syncService.addRectangle(rect); return this.map.delete(lastPoint); } @@ -89,7 +122,7 @@ export class MapComponent implements OnInit { }; unsub = () => { - if(this.sub) { + if (this.sub) { this.sub.unsubscribe(); this.sub = null; } @@ -97,26 +130,51 @@ export class MapComponent implements OnInit { menu: ToolbarItem[] = [ {name: 'Marker', icon: 'room', toggle: true, onEnabled: this.startMarker, onDisabled: this.unsub}, - {name: 'Draw', icon: 'create', toggle: true, onEnabled: () => this.startDrawing(), onDisabled: () => this.stopDrawing()}, + {name: 'Draw', icon: 'create', toggle: true, onEnabled: this.startDrawing, onDisabled: this.unsub}, {name: 'Circle', icon: 'panorama_fish_eye', toggle: true, onEnabled: this.startCircle, onDisabled: this.unsub}, {name: 'Square', icon: 'crop_square', toggle: true, onEnabled: this.startRectangle, onDisabled: this.unsub}, {name: 'Polygon', icon: 'details', toggle: true}, - {name: 'Measure', icon: 'straighten', toggle: true, onEnabled: this.startMeasuring, onDisabled: () => this.unsub}, + { + name: 'Measure', + icon: 'straighten', + toggle: true, + onEnabled: this.startMeasuring, + onDisabled: () => this.unsub + }, {name: 'Delete', icon: 'delete', toggle: true, onEnabled: this.startDelete, onDisabled: this.unsub}, - {name: 'Map Style', icon: 'terrain', subMenu: [ - {name: 'ESRI:Topographic', toggle: true, click: () => this.map.setMapLayer(MapLayers.ESRI_TOPOGRAPHIC)}, - {name: 'ESRI:Satellite', toggle: true, click: () => this.map.setMapLayer(MapLayers.ESRI_IMAGERY)}, - {name: 'ESRI:Satellite Clear', toggle: true, enabled: true, click: () => this.map.setMapLayer(MapLayers.ESRI_IMAGERY_CLARITY)} - ]}, - {name: 'Weather', icon: 'cloud', subMenu: [ + { + name: 'Map Style', icon: 'terrain', subMenu: [ + {name: 'ESRI:Topographic', toggle: true, click: () => this.map.setMapLayer(MapLayers.ESRI_TOPOGRAPHIC)}, + {name: 'ESRI:Satellite', toggle: true, click: () => this.map.setMapLayer(MapLayers.ESRI_IMAGERY)}, + { + name: 'ESRI:Satellite Clear', + toggle: true, + enabled: true, + click: () => this.map.setMapLayer(MapLayers.ESRI_IMAGERY_CLARITY) + } + ] + }, + { + name: 'Weather', icon: 'cloud', subMenu: [ {name: 'None', toggle: true, enabled: true, click: () => this.map.setWeatherLayer()}, {name: 'Temperature', toggle: true, click: () => this.map.setWeatherLayer(WeatherLayers.TEMP_NEW)}, {name: 'Wind', toggle: true, click: () => this.map.setWeatherLayer(WeatherLayers.WIND_NEW)}, - {name: 'Sea Level Pressure', toggle: true, click: () => this.map.setWeatherLayer(WeatherLayers.SEA_LEVEL_PRESSURE)}, + { + name: 'Sea Level Pressure', + toggle: true, + click: () => this.map.setWeatherLayer(WeatherLayers.SEA_LEVEL_PRESSURE) + }, {name: 'Clouds', toggle: true, click: () => this.map.setWeatherLayer(WeatherLayers.CLOUDS_NEW)}, - ]}, + ] + }, {name: 'Calibrate', icon: 'explore', toggle: true, onEnabled: this.startCalibrating, onDisabled: this.unsub}, - {name: 'Share', icon: 'share', toggle: true, onEnabled: () => this.share(), onDisabled: () => this.shareDialog = false}, + { + name: 'Share', + icon: 'share', + toggle: true, + onEnabled: () => this.share(), + onDisabled: () => this.shareDialog = false + }, {name: 'Messages', icon: 'chat', hidden: true}, {name: 'Identity', icon: 'perm_identity', hidden: true}, {name: 'Settings', icon: 'settings', hidden: true}, @@ -131,10 +189,11 @@ export class MapComponent implements OnInit { // Handle drawing the map after updates this.syncService.mapSymbols.pipe(filter(s => !!s)).subscribe((map: MapData) => { this.map.deleteAll(); - if(map.circles) map.circles.forEach(c => this.map.newCircle(c)); - if(map.markers) map.markers.forEach(m => this.map.newMarker(m)); - if(map.measurements) map.measurements.forEach(m => this.map.newMeasurement(m)); - if(map.rectangles) map.rectangles.forEach(r => this.map.newRectangle(r)); + if (map.circles) map.circles.forEach(c => this.map.newCircle(c)); + if (map.markers) map.markers.forEach(m => this.map.newMarker(m)); + if (map.measurements) map.measurements.forEach(m => this.map.newMeasurement(m)); + if (map.polylines) map.polylines.forEach(p => this.map.newPolyline(p)); + if (map.rectangles) map.rectangles.forEach(r => this.map.newRectangle(r)); }) }) } @@ -144,11 +203,11 @@ export class MapComponent implements OnInit { // Handle opening symbols this.map.click.pipe(filter(e => !!e && e.item)).subscribe(e => { - if(e.item instanceof L.Marker) { - if(e.symbol.noSelect) return; + if (e.item instanceof L.Marker) { + if (e.symbol.noSelect) return; /*this.bottomSheet.open(MarkerComponent, {data: e.symbol, hasBackdrop: false, disableClose: true});*/ - } else if(e.item instanceof L.Circle) { - if(e.symbol.noSelect) return; + } else if (e.item instanceof L.Circle) { + if (e.symbol.noSelect) return; /*this.bottomSheet.open(CircleComponent, {data: e.symbol, hasBackdrop: false, disableClose: true}).afterDismissed().subscribe(c => { let circle = c['_symbol']; this.map.delete(c); @@ -159,23 +218,40 @@ export class MapComponent implements OnInit { // Display location this.physicsService.info.pipe(filter(coord => !!coord)).subscribe(pos => { - if(!this.position) this.center({lat: pos.latitude, lng: pos.longitude}); - if(this.positionMarker.arrow) this.map.delete(this.positionMarker.arrow); - if(this.positionMarker.circle) this.map.delete(this.positionMarker.circle); - this.positionMarker.arrow = this.map.newMarker({latlng: {lat: pos.latitude, lng: pos.longitude}, noSelect: true, noDelete: true, icon: 'arrow', rotationAngle: pos.heading, rotationOrigin: 'center'}); - this.positionMarker.circle = this.map.newCircle({latlng: {lat: pos.latitude, lng: pos.longitude}, color: '#2873d8', noSelect: true, noDelete: true, radius: pos.accuracy, interactive: false}); + if (!this.position) this.center({lat: pos.latitude, lng: pos.longitude}); + if (this.positionMarker.arrow) this.map.delete(this.positionMarker.arrow); + if (this.positionMarker.circle) this.map.delete(this.positionMarker.circle); + this.positionMarker.arrow = this.map.newMarker({ + latlng: {lat: pos.latitude, lng: pos.longitude}, + noSelect: true, + noDelete: true, + icon: 'arrow', + rotationAngle: pos.heading, + rotationOrigin: 'center' + }); + this.positionMarker.circle = this.map.newCircle({ + latlng: {lat: pos.latitude, lng: pos.longitude}, + color: '#2873d8', + noSelect: true, + noDelete: true, + radius: pos.accuracy, + interactive: false + }); this.position = pos; }); // Calibration popup this.physicsService.requireCalibration.subscribe(() => { - this.snackBar.open('Compass requires calibration', 'calibrate', {duration: 5000, panelClass: 'bg-warning,text-white'}) + this.snackBar.open('Compass requires calibration', 'calibrate', { + duration: 5000, + panelClass: 'bg-warning,text-white' + }) .onAction().subscribe(() => this.startCalibrating()); }); } center(pos?) { - if(!pos) pos = {lat: this.position.latitude, lng: this.position.longitude}; + if (!pos) pos = {lat: this.position.latitude, lng: this.position.longitude}; this.map.centerOn(pos); } @@ -186,7 +262,7 @@ export class MapComponent implements OnInit { share() { this.shareDialog = true; - if(navigator['share']) { + if (navigator['share']) { navigator['share']({ title: 'Map Alliance', text: 'A map alliance has been requested!', @@ -194,14 +270,4 @@ export class MapComponent implements OnInit { }) } } - - startDrawing() { - this.showPalette = true; - this.map.startDrawing(); - } - - stopDrawing() { - this.showPalette = false; - this.map.stopDrawing() - } }