Edit lines using routing form (fixes #17)

pull/108/head
Candid Dauth 2017-05-25 22:58:29 +02:00
rodzic bd820f9fbf
commit fd73b7a60d
11 zmienionych plików z 217 dodań i 181 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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<p.length; i++) {
if(p[i] != null)
trackPoints.push(L.latLng(p[i].lat, p[i].lon));
@ -68,7 +67,7 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
linesById[line.id] = L.polyline([ ]).addTo(map.map);
map.map.almostOver.addLayer(linesById[line.id]);
if(line.id != null && line.id != editingLineId) { // We don't want a popup for lines that we are drawing right now
if(line.id != null) { // We don't want a popup for lines that we are drawing right now
linesById[line.id]
.on("click", function(e) {
linesUi.showLineInfoBox(map.client.lines[line.id]);
@ -202,123 +201,6 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
template.filter(".content").on("resizeend", drawElevationPlot);
setTimeout(drawElevationPlot, 0);
},
_makeLineMovable: function(line) {
var markers = [ ];
editingLineId = line.id;
// Re-add the line (because editingLineId is set)
linesUi._deleteLine(line);
linesUi._addLine(line);
// Watch if route points change (because someone else has moved the line while we are moving it
var routePointsBkp = ng.copy(line.routePoints);
var unregisterWatcher = $rootScope.$watch(() => (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<markers.length; i++)
markers[i].remove();
markers = [ ];
}
// This marker is shown when we hover the line. It enables us to create new markers.
// It is a huge one (a normal marker with 5000 px or so transparency around it, so that we can be
// sure that the mouse is over it and dragging it will work smoothly.
var temporaryHoverMarker;
function _over(e) {
temporaryHoverMarker.setLatLng(e.latlng).addTo(map.map);
}
function _move(e) {
temporaryHoverMarker.setLatLng(e.latlng);
}
function _out(e) {
temporaryHoverMarker.remove();
}
linesById[line.id].on("fm-almostover", _over).on("fm-almostmove", _move).on("fm-almostout", _out);
function makeTemporaryHoverMarker() {
temporaryHoverMarker = createTempMarker(true);
temporaryHoverMarker.once("dragstart", function() {
temporaryHoverMarker.once("dragend", function() {
// We have to replace the huge icon with the regular one at the end of the dragging, otherwise
// the dragging gets interrupted
this.setIcon(fmUtils.createMarkerIcon("ffd700", 35));
}, temporaryHoverMarker);
var latlng = temporaryHoverMarker.getLatLng();
var idx = fmUtils.getIndexOnLine(map.map, line.routePoints, line.routePoints, latlng);
markers.splice(idx, 0, temporaryHoverMarker);
line.routePoints.splice(idx, 0, { lat: latlng.lat, lon: latlng.lng });
makeTemporaryHoverMarker();
});
}
makeTemporaryHoverMarker();
return {
done : function() {
editingLineId = null;
unregisterWatcher();
removeTempMarkers();
temporaryHoverMarker.remove();
// Re-add the line (because editingLineId is not set anymore)
linesUi._deleteLine(line);
linesUi._addLine(line);
return line.routePoints;
}
};
},
editLine: function(line) {
var scope = $rootScope.$new();
scope.client = map.client;
@ -406,29 +288,58 @@ fm.app.factory("fmMapLines", function(fmUtils, $uibModal, $compile, $timeout, $r
});
},
moveLine: function(line) {
var movable = linesUi._makeLineMovable(line);
var 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) },
{ label: "Cancel", click: done.bind(null, false) }
], null, done.bind(null, false, true));
map.routeUi.lineToRoute(line.id).then(() => {
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) {

Wyświetl plik

@ -2,7 +2,7 @@
<div ng-repeat="message in messages" uib-alert ng-class="'alert-' + message.type" close="message.close(true)">
<strong>{{message.message}}</strong>
<div class="btn-group pull-right btn-group-sm">
<a ng-repeat="button in message.buttons" href="{{button.url || 'javascript:'}}" ng-click="button.click && button.click()" class="btn btn-default">{{button.label}}</a>
<a ng-repeat="button in message.buttons" href="{{button.url || 'javascript:'}}" ng-click="button.click && button.click()" class="btn btn-default" ng-disabled="button.enabled && !button.enabled()">{{button.label}}</a>
</div>
</div>
</div>

Wyświetl plik

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

Wyświetl plik

@ -8,9 +8,9 @@
</dl>
<div class="fm-elevation-plot" ng-show="client.route.ascent != null"></div>
</div>
<div class="buttons" ng-if="!client.readonly">
<div class="buttons" ng-if="!client.readonly && !client._editingLineId">
<div uib-dropdown keyboard-nav="true" ng-if="(client.types | fmPropertyCount:{type:'line'}) > 1" class="dropup">
<button id="add-type-button" type="button" class="btn btn-default" uib-dropdown-toggle>Add to map <span class="caret"></span></button>
<button id="add-type-button" type="button" class="btn btn-default btn-sm" uib-dropdown-toggle>Add to map <span class="caret"></span></button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="add-type-button" uib-dropdown-menu>
<li role="menuitem" ng-repeat="type in client.types | fmObjectFilter:{type:'line'}"><a href="javascript:" ng-click="addToMap(type)">{{type.name}}</a></li>
</ul>

Wyświetl plik

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

Wyświetl plik

@ -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<queries.length; i++) {
if(scope.destinations.length <= i)
@ -209,6 +211,9 @@ fm.app.factory("fmSearchRoute", function($rootScope, $compile, fmUtils, $timeout
$.extend(scope.destinations[i], typeof queries[i] == "object" ? queries[i] : { query: queries[i] });
}
while(scope.destinations.length < 2)
scope.addDestination();
},
setFrom: function(from, suggestions, selectedSuggestion) {

Wyświetl plik

@ -123,7 +123,7 @@ module.exports = function(Database) {
return this._getPadObject("Line", padId, lineId);
},
createLine(padId, data) {
createLine(padId, data, trackPointsFromRoute) {
return utils.promiseAuto({
defaultVals: this.getType(padId, data.typeId).then((type) => {
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;

Wyświetl plik

@ -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"]
});
}
});
};

Wyświetl plik

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