From fd73b7a60daddc13cb818a4214dfe446fc3ac6ab Mon Sep 17 00:00:00 2001 From: Candid Dauth Date: Thu, 25 May 2017 22:58:29 +0200 Subject: [PATCH] Edit lines using routing form (fixes #17) --- client/API.md | 9 ++ client/client.js | 9 ++ frontend/app/map/lines/lines.js | 187 +++++++----------------- frontend/app/map/messages/messages.html | 2 +- frontend/app/map/route/route.js | 9 ++ frontend/app/map/route/view-route.html | 4 +- frontend/app/search/search-query.js | 5 + frontend/app/search/search-route.js | 7 +- server/database/line.js | 25 +++- server/database/route.js | 79 +++++++--- server/socket.js | 62 ++++++-- 11 files changed, 217 insertions(+), 181 deletions(-) diff --git a/client/API.md b/client/API.md index b1af6e66..b065c374 100644 --- a/client/API.md +++ b/client/API.md @@ -355,6 +355,15 @@ Clear the temporary route set via [`setRoute(data)`](#setroutedata). * _returns_ (Promise) +### `lineToRoute(data)` + +Call [`setRoute()`](#setroutedata) with the parameters of an existing line. Saves time, as the route does not need to be +recalculated. + +* `data` (object): An object with the following properties: + * `id` (string): The ID of the line +* _returns_ (Promise<[route](#route-1)>) + ### `addMarker(data)` Create a marker. diff --git a/client/client.js b/client/client.js index 803cd851..cc56d7d5 100644 --- a/client/client.js +++ b/client/client.js @@ -168,6 +168,15 @@ class Socket { return this._emit("clearRoute"); } + lineToRoute(data) { + return this._emit("lineToRoute", data).then((route) => { + this.route = route; + this.route.trackPoints = this._mergeTrackPoints({}, route.trackPoints); + + return this.route; + }); + } + addType(data) { return this._emit("addType", data); } diff --git a/frontend/app/map/lines/lines.js b/frontend/app/map/lines/lines.js index aae883de..9ded4e50 100644 --- a/frontend/app/map/lines/lines.js +++ b/frontend/app/map/lines/lines.js @@ -9,7 +9,6 @@ import css from './lines.scss'; fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $rootScope) { return function(map) { var linesById = { }; - var editingLineId = null; let openLine = null; let openLineHighlight = null; @@ -21,7 +20,7 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r map.client.on("line", function(data) { setTimeout(function() { // trackPoints needs to be copied over - if(map.client.filterFunc(map.client.lines[data.id])) + if((!map.client._editingLineId || data.id != map.client._editingLineId) && map.client.filterFunc(map.client.lines[data.id])) linesUi._addLine(map.client.lines[data.id]); }, 0); }); @@ -32,14 +31,14 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r map.client.on("linePoints", function(data) { setTimeout(function() { - if(map.client.filterFunc(map.client.lines[data.id])) + if((!map.client._editingLineId || data.id != map.client._editingLineId) && map.client.filterFunc(map.client.lines[data.id])) linesUi._addLine(map.client.lines[data.id]); }, 0); }); map.client.on("filter", function() { for(var i in map.client.lines) { - var show = map.client.filterFunc(map.client.lines[i]); + var show = (!map.client._editingLineId || i != map.client._editingLineId) && map.client.filterFunc(map.client.lines[i]); if(linesById[i] && !show) linesUi._deleteLine(map.client.lines[i]); else if(!linesById[i] && show) @@ -55,7 +54,7 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r var linesUi = { _addLine: function(line, _doNotRerenderPopup) { var trackPoints = [ ]; - var p = (editingLineId != null && editingLineId == line.id ? line.routePoints : line.trackPoints) || [ ]; + var p = line.trackPoints || [ ]; for(var i=0; i (map.client.lines[line.id].routePoints), function() { - // We do not do a deep watch, as then we will be not notified if someone edits the line without - // actually moving it, in which case we still need to redraw it (because it gets redrawn because - // the server sends it to us again). - - // The line has been edited, but it has not been moved. Override its points with our current stage again - if(ng.equals(routePointsBkp, map.client.lines[line.id].routePoints)) - map.client.lines[line.id].routePoints = line.routePoints; - else // The line has been moved. Override our stage with the new points. - routePointsBkp = ng.copy(line.routePoints); - - line = map.client.lines[line.id]; - linesUi._addLine(line); - removeTempMarkers(); - createTempMarkers(); - }); - - function createTempMarker(huge) { - var marker = L.marker([0,0], { - icon: fmUtils.createMarkerIcon(map.dragMarkerColour, 35, null, huge ? 1000 : null), - draggable: true - }) - .on("dblclick", function() { - // Double click on temporary marker: Remove this route point - var idx = markers.indexOf(marker); - markers.splice(idx, 1); - line.routePoints.splice(idx, 1); - marker.remove(); - linesUi._addLine(line); - }) - .on("drag", function() { - var idx = markers.indexOf(marker); - var latlng = marker.getLatLng(); - line.routePoints[idx] = { lat: latlng.lat, lon: latlng.lng }; - linesUi._addLine(line); - }); - return marker; - } - - function createTempMarkers() { - line.routePoints.forEach(function(it) { - markers.push(createTempMarker().setLatLng([ it.lat, it.lon ]).addTo(map.map)); - }); - } - - function removeTempMarkers() { - for(var i=0; i { + map.client._editingLineId = line.id; + linesUi._deleteLine(line); - function done(save, noClose) { - var newPoints = movable.done(); - linesUi._addLine(line); - linesUi.showLineInfoBox(line); + let message = map.messages.showMessage("info", "Drag the line points around to change it. Double-click a point to remove it.", [ + { label: "Finish", click: done.bind(null, true), enabled: () => (!!map.client.route) }, + { label: "Cancel", click: done.bind(null, false) } + ], null, done.bind(null, false, true)); - if(!noClose) { - message.close(); + let searchBkp; + if(map.searchUi) { + searchBkp = map.searchUi.getCurrentSearchForHash() || ""; + map.searchUi.route(map.client.route.routePoints.map((routePoint) => (fmUtils.round(routePoint.lat, 5) + "," + fmUtils.round(routePoint.lon, 5))), map.client.route.mode, false, true); } - if(save) { - line.trackPoints = { }; - map.client.editLine({ id: line.id, routePoints: newPoints }).catch(function(err) { + function done(save, noClose) { + map.client._editingLineId = null; + linesUi._addLine(map.client.lines[line.id]); + linesUi.showLineInfoBox(map.client.lines[line.id]); + + if(!noClose) { + message.close(); + } + + if(save && !map.client.route) { + map.messages.showMessage("danger", "No route set."); + return; + } + + Promise.resolve().then(() => { + if(save) { + return map.client.editLine({ id: line.id, routePoints: map.client.route.routePoints, mode: map.client.route.mode }); + } + }).then(() => { + // Clear route after editing line so that the server can take the trackPoints from the route + let ret = map.routeUi.clearRoute(); + + if(map.searchUi) { + map.searchUi.route([], null, false, true); + map.searchUi.search(searchBkp, true); + } + + return ret; + }).catch(function(err) { map.messages.showMessage("danger", err); }); } - } + }).catch((err) => { + console.log("err", err); + map.messages.showMessage("danger", err); + }); }, deleteLine: function(line) { map.client.deleteLine(line).catch(function(err) { diff --git a/frontend/app/map/messages/messages.html b/frontend/app/map/messages/messages.html index 54b8ccec..49e1499a 100644 --- a/frontend/app/map/messages/messages.html +++ b/frontend/app/map/messages/messages.html @@ -2,7 +2,7 @@
{{message.message}}
\ No newline at end of file diff --git a/frontend/app/map/route/route.js b/frontend/app/map/route/route.js index 7a257c19..0b1a8978 100644 --- a/frontend/app/map/route/route.js +++ b/frontend/app/map/route/route.js @@ -275,6 +275,15 @@ fm.app.factory("fmMapRoute", function(fmUtils, $uibModal, $compile, $timeout, $r }); }, + lineToRoute(lineId) { + return map.client.lineToRoute({ + id: lineId + }).then(() => { + renderRoute(); + this.showRouteInfoBox(); + }); + }, + clearRoute() { map.mapEvents.$broadcast("routeClear"); diff --git a/frontend/app/map/route/view-route.html b/frontend/app/map/route/view-route.html index 1e2359d9..6021bb2a 100644 --- a/frontend/app/map/route/view-route.html +++ b/frontend/app/map/route/view-route.html @@ -8,9 +8,9 @@
-
+
- + diff --git a/frontend/app/search/search-query.js b/frontend/app/search/search-query.js index f08f5a9c..dc57fd6d 100644 --- a/frontend/app/search/search-query.js +++ b/frontend/app/search/search-query.js @@ -358,6 +358,11 @@ fm.app.factory("fmSearchQuery", function($rootScope, $compile, fmUtils, $timeout }, search: function(query, noZoom, showAll) { + if(!searchUi._el.is(":visible")) { + routeUi.hide(); + searchUi.show(); + } + if(query != null) scope.searchString = query; diff --git a/frontend/app/search/search-route.js b/frontend/app/search/search-route.js index d3b30ba9..0bf3b747 100644 --- a/frontend/app/search/search-route.js +++ b/frontend/app/search/search-route.js @@ -201,7 +201,9 @@ fm.app.factory("fmSearchRoute", function($rootScope, $compile, fmUtils, $timeout }, setQueries: function(queries) { - scope.clear(); + scope.submittedQueries = null; + scope.submittedMode = null; + scope.destinations = [ ]; for(var i=0; i { if(type.defaultColour && !("colour" in data)) @@ -135,7 +135,7 @@ module.exports = function(Database) { }), routing: (defaultVals) => { - return this._calculateRouting(data); + return this._calculateRouting(data, trackPointsFromRoute); }, createLine: (routing, defaultVals) => { @@ -163,7 +163,7 @@ module.exports = function(Database) { }); }, - updateLine(padId, lineId, data, doNotUpdateStyles) { + updateLine(padId, lineId, data, doNotUpdateStyles, trackPointsFromRoute) { return utils.promiseAuto({ originalLine: this.getLine(padId, lineId), @@ -175,7 +175,7 @@ module.exports = function(Database) { data.mode = originalLine.mode || ""; if((data.mode == "track" && data.trackPoints) || !underscore.isEqual(data.routePoints, originalLine.routePoints) || data.mode != originalLine.mode) - return this._calculateRouting(data); // Also sets data.distance and data.time + return this._calculateRouting(data, trackPointsFromRoute); // Also sets data.distance and data.time }, newLine: (routing) => { @@ -279,8 +279,21 @@ module.exports = function(Database) { }); }, - _calculateRouting(line) { - if(line.mode == "track" && line.trackPoints && line.trackPoints.length >= 2) { + getAllLinePoints(lineId) { + return this._conn.model("Line").build({ id: lineId }).getLinePoints({ + attributes: [ "lat", "lon", "ele", "zoom", "idx" ] + }); + }, + + _calculateRouting(line, trackPointsFromRoute) { + if(trackPointsFromRoute) { + line.distance = trackPointsFromRoute.distance; + line.time = trackPointsFromRoute.time; + line.ascent = trackPointsFromRoute.ascent; + line.descent = trackPointsFromRoute.descent; + + return Promise.resolve(trackPointsFromRoute.trackPoints); + } else if(line.mode == "track" && line.trackPoints && line.trackPoints.length >= 2) { line.distance = utils.calculateDistance(line.trackPoints); line.time = null; diff --git a/server/database/route.js b/server/database/route.js index c64de8e2..96b75eb4 100644 --- a/server/database/route.js +++ b/server/database/route.js @@ -38,26 +38,22 @@ module.exports = function(Database) { if(getCompleteBasicRoute) cond.$or.push({ zoom: { lte: 5 } }); - let ret = new utils.ArrayStream(); - - this._conn.model("RoutePoint").findAll({ + return this._conn.model("RoutePoint").findAll({ where: cond, attributes: [ "lon", "lat", "idx", "ele"], order: "idx" - }).then((objs) => { - ret.receiveArray(null, objs); - }).catch((err) => { - ret.receiveArray(err); }); + }, - return ret; + generateRouteId() { + // TODO: Check if exists + return Promise.resolve(utils.generateRandomId(20)); }, createRoute(routePoints, mode, calculateElevation) { - // TODO: Check if exists - let routeId = utils.generateRandomId(20); - - return this.updateRoute(routeId, routePoints, mode, calculateElevation, true); + return this.generateRouteId().then((routeId) => { + return this.updateRoute(routeId, routePoints, mode, calculateElevation, true); + }); }, updateRoute(routeId, routePoints, mode, calculateElevation, _noClear) { @@ -113,6 +109,50 @@ module.exports = function(Database) { }); }, + lineToRoute(routeId, padId, lineId) { + return utils.promiseAuto({ + routeId: () => (routeId ? routeId : this.generateRouteId()), + clear: () => { + if(routeId) { + return this._conn.model("RoutePoint").destroy({ + where: { routeId } + }); + } + }, + line: () => (this.getLine(padId, lineId)), + linePoints: () => (this.getAllLinePoints(lineId)), + update: (routeId, clear, line, linePoints) => { + let create = []; + for(let linePoint of linePoints) { + create.push({ + routeId, + lat: linePoint.lat, + lon: linePoint.lon, + ele: linePoint.ele, + zoom: linePoint.zoom, + idx: linePoint.idx + }); + } + + return this._bulkCreateInBatches(this._conn.model("RoutePoint"), create); + } + + }).then((res) => { + updateTimes[res.routeId] = Date.now(); + + return { + id: res.routeId, + mode: res.line.mode, + routePoints: res.line.routePoints, + trackPoints: res.linePoints, + distance: res.line.distance, + time: res.line.time, + ascent: res.line.ascent, + descent: res.line.descent + }; + }); + }, + deleteRoute(routeId) { delete updateTimes[routeId]; @@ -124,19 +164,18 @@ module.exports = function(Database) { }, getRoutePointsByIdx(routeId, indexes) { - let ret = new utils.ArrayStream(); - - this._conn.model("RoutePoint").findAll({ + return this._conn.model("RoutePoint").findAll({ where: { routeId, idx: indexes }, attributes: [ "lon", "lat", "idx", "ele" ], order: "idx" - }).then((objs) => { - ret.receiveArray(null, objs); - }).catch((err) => { - ret.receiveArray(err); }); + }, - return ret; + getAllRoutePoints(routeId) { + return this._conn.model("RoutePoint").findAll({ + where: {routeId}, + attributes: [ "lon", "lat", "idx", "ele", "zoom"] + }); } }); }; \ No newline at end of file diff --git a/server/socket.js b/server/socket.js index 78d1bb5e..58aaf23e 100644 --- a/server/socket.js +++ b/server/socket.js @@ -1,6 +1,7 @@ var socketIo = require("socket.io"); var domain = require("domain"); var Promise = require("bluebird"); +var underscore = require("underscore"); var utils = require("./utils"); var routing = require("./routing"); @@ -33,7 +34,7 @@ class SocketConnection { this.bbox = null; this.writable = null; - this.routeId = null; + this.route = null; this._dbHandlers = [ ]; @@ -188,8 +189,8 @@ utils.extend(SocketConnection.prototype, { ret.marker = utils.streamToArrayPromise(this.database.getPadMarkers(this.padId, bboxWithExcept)); ret.linePoints = utils.streamToArrayPromise(this.database.getLinePointsForPad(this.padId, bboxWithExcept)); } - if(this.routeId) - ret.routePoints = utils.streamToArrayPromise(this.database.getRoutePoints(this.routeId, bboxWithExcept, !bboxWithExcept.except)).then((points) => ([points])); + if(this.route) + ret.routePoints = this.database.getRoutePoints(this.route.id, bboxWithExcept, !bboxWithExcept.except).then((points) => ([points])); return Promise.props(ret); }, @@ -198,8 +199,8 @@ utils.extend(SocketConnection.prototype, { if(this.padId) this.unregisterDatabaseHandlers(); - if(this.routeId) { - this.database.deleteRoute(this.routeId).catch((err) => { + if(this.route) { + this.database.deleteRoute(this.route.id).catch((err) => { console.error("Error clearing route", err.stack || err); }); } @@ -289,7 +290,10 @@ utils.extend(SocketConnection.prototype, { if(!this.writable) throw "In read-only mode."; - return this.database.createLine(this.padId, data); + if(this.route && data.mode != "track" && underscore.isEqual(this.route.routePoints, data.routePoints)) + return this.database.getAllRoutePoints(this.route.id); + }).then((trackPoints) => { + return this.database.createLine(this.padId, data, trackPoints && Object.assign({}, this.route, {trackPoints})); }); }, @@ -301,7 +305,10 @@ utils.extend(SocketConnection.prototype, { if(!this.writable) throw "In read-only mode."; - return this.database.updateLine(this.padId, data.id, data); + if(this.route && data.mode != "track" && underscore.isEqual(this.route.routePoints, data.routePoints)) + return this.database.getAllRoutePoints(this.route.id); + }).then((trackPoints) => { + return this.database.updateLine(this.padId, data.id, data, null, trackPoints && Object.assign({}, this.route, {trackPoints})); }); }, @@ -476,8 +483,8 @@ utils.extend(SocketConnection.prototype, { if(!utils.stripObject(data, { routePoints: [ { lat: "number", lon: "number" } ], mode: "string", elevation: "boolean" })) throw "Invalid parameters."; - if(this.routeId) - return this.database.updateRoute(this.routeId, data.routePoints, data.mode, data.elevation); + if(this.route) + return this.database.updateRoute(this.route.id, data.routePoints, data.mode, data.elevation); else return this.database.createRoute(data.routePoints, data.mode, data.elevation); }).then((routeInfo) => { @@ -487,7 +494,7 @@ utils.extend(SocketConnection.prototype, { return; } - this.routeId = routeInfo.id; + this.route = routeInfo; if(this.bbox) routeInfo.trackPoints = routing.prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true); @@ -508,10 +515,39 @@ utils.extend(SocketConnection.prototype, { clearRoute: function() { return Promise.resolve().then(() => { - if(this.routeId) - return this.database.deleteRoute(this.routeId); + if(this.route) + return this.database.deleteRoute(this.route.id); }).then(() => { - this.routeId = null; + this.route = null; + }); + }, + + lineToRoute: function(data) { + return Promise.resolve().then(() => { + if(!utils.stripObject(data, { id: "string" })) + throw "Invalid parameters."; + + if(!this.padId) + throw "No collaborative map opened."; + + return this.database.lineToRoute(this.route && this.route.id, this.padId, data.id); + }).then((routeInfo) => { + this.route = routeInfo; + + if(this.bbox) + routeInfo.trackPoints = routing.prepareForBoundingBox(routeInfo.trackPoints, this.bbox, true); + else + routeInfo.trackPoints = []; + + return { + routePoints: routeInfo.routePoints, + mode: routeInfo.mode, + time: routeInfo.time, + distance: routeInfo.distance, + ascent: routeInfo.ascent, + descent: routeInfo.descent, + trackPoints: routeInfo.trackPoints + }; }); },