Basic support to calculate routes using search form

pull/54/merge
Candid Dauth 2016-10-12 13:51:16 +03:00
rodzic 0a1dbf4461
commit 30fe847ba4
13 zmienionych plików z 437 dodań i 22 usunięć

Wyświetl plik

@ -0,0 +1,17 @@
<div class="content container-fluid">
<h2>{{route.short_name}}</h2>
<dl class="dl-horizontal">
<dt class="distance">Distance</dt>
<dd class="distance">{{route.distance | fpRound:2}} km ({{route.time | fpFormatTime}} h {{route.routeMode | fpRoutingMode}})</dd>
</dl>
</div>
<div class="buttons" ng-if="!readonly">
<div uib-dropdown ng-if="(types | fpPropertyCount:{type:'line'}) > 1">
<button id="add-type-button" type="button" class="btn btn-default" 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 types | fpObjectFilter:{type:'line'}"><a href="#" ng-click="addToMap(type)">{{type.name}}</a></li>
</ul>
</div>
<button ng-if="(types | fpPropertyCount:{type:'line'}) == 1" class="btn btn-default" ng-click="addToMap()">Add to map</button>
</div>

Wyświetl plik

@ -0,0 +1,47 @@
<div class="fp-search panel panel-default" ng-class="{'fp-hasResults': routes.length > 0}">
<div class="panel-body">
<form ng-submit="route()" ui-sortable="sortableOptions" ng-model="destinations">
<div class="form-group" ng-repeat="destination in destinations">
<div class="input-group">
<span class="input-group-addon">
<a href="javascript:" class="sort-handle"><span class="glyphicon glyphicon-resize-vertical"></span></a>
<a href="javascript:" ng-click="removeDestination($index)" ng-show="destinations.length > 2"><span class="glyphicon glyphicon-minus"></span></a>
</span>
<div class="has-feedback">
<input type="search" class="form-control" ng-model="destination.query" placeholder="{{$index == 0 ? 'From' : $index == destinations.length-1 ? 'To' : 'Via'}}" tabindex="{{$index+1}}">
<a href="javascript:" class="reset-button form-control-feedback" ng-click="destination.query=''" ng-show="destination.query.length > 0"><span class="icon-clear"></span></a>
</div>
<span class="input-group-btn" uib-dropdown ng-show="destination.query.length > 0">
<button type="button" class="btn btn-default" uib-dropdown-toggle ng-click="loadSuggestions(destination)"><span class="caret"></span></button>
<ul class="dropdown-menu" role="menu" uib-dropdown-menu>
<li ng-repeat="suggestion in destination.suggestions" ng-class="{active: destination.selectedSuggestionIdx == $index}"><a href="javascript:" ng-click="destination.selectedSuggestionIdx = $index; reroute()">{{suggestion.display_name}} ({{suggestion.type}})</a></li>
</ul>
</span>
</div>
</div>
<div class="form-group">
<button type="button" class="btn btn-default" ng-click="addDestination()"><span class="glyphicon glyphicon-plus"></span></button>
<div class="btn-group">
<button type="button" class="btn btn-default" uib-btn-radio="'car'" ng-model="routeMode" tabindex="{{destinations.length+1}}" ng-click="reroute()"><span class="icon-car"></span></button>
<button type="button" class="btn btn-default" uib-btn-radio="'bicycle'" ng-model="routeMode" tabindex="{{destinations.length+2}}" ng-click="reroute()"><span class="icon-bicycle"></span></button>
<button type="button" class="btn btn-default" uib-btn-radio="'pedestrian'" ng-model="routeMode" tabindex="{{destinations.length+3}}" ng-click="reroute()"><span class="icon-walk"></span></button>
</div>
<button type="submit" class="btn btn-primary" tabindex="{{destinations.length+4}}">Go!</button>
<button type="button" class="btn btn-default pull-right active" ng-click="showSearchForm()"><span class="glyphicon glyphicon-road"></span></button>
</div>
<div class="fp-search-results" ng-show="routes || routeError">
<div uib-alert class="alert-danger no-results" ng-if="routeError">{{routeError}}</div>
<ul class="list-group" ng-if="routes.length > 0">
<li ng-repeat="route in routes" class="list-group-item" ng-class="{ active: activeRouteIdx == $index }">
<a href="javascript:" ng-click="setActiveRoute($index)">{{route.display_name}}</a>
</li>
</ul>
</div>
</form>
</div>
</div>

Wyświetl plik

@ -0,0 +1,213 @@
(function(fp, $, ng, undefined) {
fp.app.factory("fpMapSearchRoute", function($rootScope, $templateCache, $compile, fpUtils, L, $timeout, $q) {
return function(map, searchUi) {
var activeStyle = {
color : '#0000ff',
weight : 8,
opacity : 0.7
};
var inactiveStyle = {
color : '#0000ff',
weight : 6,
opacity : 0.3
};
var scope = $rootScope.$new(true);
scope.routeMode = 'car';
scope.destinations = [ ];
scope.sortableOptions = ng.copy($rootScope.sortableOptions);
scope.sortableOptions.update = function() {
scope.reroute();
};
scope.addDestination = function() {
scope.destinations.push({
query: "",
suggestions: [ ]
});
};
scope.addDestination();
scope.addDestination();
scope.removeDestination = function(idx) {
scope.destinations.splice(idx, 1);
};
scope.showSearchForm = function() {
routeUi.hide();
searchUi.show();
};
scope.loadSuggestions = function(destination) {
return $q(function(resolve, reject) {
if(destination.suggestionQuery == destination.query)
return resolve();
destination.suggestions = [ ];
destination.suggestionQuery = null;
destination.selectedSuggestionIdx = null;
if(destination.query.trim() != "") {
var query = destination.query;
map.loadStart();
map.socket.emit("find", { query: query }, function(err, results) {
map.loadEnd();
if(err) {
map.messages.showMessage("danger", err);
return reject(err);
}
destination.suggestions = results;
destination.suggestionQuery = query;
destination.selectedSuggestionIdx = 0;
resolve();
});
}
});
};
scope.route = function() {
scope.reset();
$q.all(scope.destinations.map(scope.loadSuggestions)).then(function() {
var points = scope.destinations.map(function(destination) {
if(destination.suggestions.length == null)
throw "No place has been found for search term “" + destination.query + "”.";
var sug = destination.suggestions[destination.selectedSuggestionIdx] || destination.suggestions[0];
return { lat: sug.lat, lon: sug.lon };
});
return $q(function(resolve, reject) {
map.socket.emit("getRoutes", { destinations: points, mode: scope.routeMode }, function(err, res) {
err ? reject(err) : resolve(res);
});
});
}).then(function(routes) {
routes.forEach(function(route, i) {
route.short_name = "Option " + (i+1);
route.display_name = route.short_name + " (" + fpUtils.round(route.distance, 2) + " km, " + fpUtils.formatTime(route.time) + " h)";
route.routeMode = scope.routeMode;
});
scope.routes = routes;
scope.activeRouteIdx = 0;
renderResults();
}).catch(function(err) {
scope.routeError = err;
});
};
scope.reroute = function() {
if(scope.routes || scope.routeError)
scope.route();
};
scope.reset = function() {
scope.routes = [ ];
scope.routeError = null;
scope.activeRouteIdx = null;
};
scope.setActiveRoute = function(routeIdx) {
scope.activeRouteIdx = routeIdx;
updateStyle();
};
var el = $($templateCache.get("map/search/search-route.html")).insertAfter(map.map.getContainer());
$compile(el)(scope);
scope.$evalAsync(); // $compile only replaces variables on next digest
var layerGroup = L.featureGroup([]).addTo(map.map);
function renderResults() {
clearRenders();
scope.routes.forEach(function(route, i) {
var layer = L.polyline(route.trackPoints.map(function(it) { return [ it.lat, it.lon ] }), i == scope.activeRouteIdx ? activeStyle : inactiveStyle)
.bindPopup($("<div/>")[0], map.popupOptions)
.on("popupopen", function(e) {
scope.setActiveRoute(i);
renderRoutePopup(route, e.popup);
}.fpWrapApply(scope))
.on("popupclose", function(e) {
ng.element(e.popup.getContent()).scope().$destroy();
})
.bindTooltip(route.display_name, $.extend({}, map.tooltipOptions, { sticky: true, offset: [ 20, 0 ] }));
layerGroup.addLayer(layer);
if(i == scope.activeRouteIdx)
layer.openPopup();
});
map.map.flyToBounds(layerGroup.getBounds());
}
function updateStyle() {
layerGroup.getLayers().forEach(function(layer, i) {
layer.setStyle(i == scope.activeRouteIdx ? activeStyle : inactiveStyle);
if(i == scope.activeRouteIdx)
layer.openPopup();
})
}
function clearRenders() {
layerGroup.clearLayers();
}
function renderRoutePopup(route, popup) {
var scope = map.socket.$new();
scope.route = route;
scope.addToMap = function(type) {
if(type == null) {
for(var i in map.socket.types) {
if(map.socket.types[i].type == "line") {
type = map.socket.types[i];
break;
}
}
}
//map.markersUi.createMarker(result, type, { name: result.display_name });
};
var el = popup.getContent();
$(el).html($templateCache.get("map/search/route-popup.html"));
$compile(el)(scope);
// Prevent popup close on button click
$("button", el).click(function(e) {
e.preventDefault();
});
$timeout(function() { $timeout(function() { // $compile only replaces variables on next digest
popup.update();
}); });
}
var routeUi = {
show: function() {
el.show();
},
hide: function() {
el.hide();
}
};
routeUi.hide();
return routeUi;
};
});
})(FacilPad, jQuery, angular);

Wyświetl plik

@ -13,6 +13,9 @@
.fp-search:hover,.fp-search.fp-hasFocus {
opacity: 1;
}
.fp-search/*.fp-hasFocus*/ {
width: 350px;
max-width: 40%;
}
@ -33,6 +36,15 @@
margin: 10px 0 0;
}
.fp-search .reset-button {
z-index: 5;
pointer-events: auto;
}
.fp-search .active a {
color: inherit;
}
@media print {
.fp-search {
display: none;

Wyświetl plik

@ -1,16 +1,20 @@
<div class="fp-search panel panel-default" ng-class="{'fp-hasResults': !!searchResults,'fp-hasFocus': !!hasFocus}">
<div class="fp-search panel panel-default" ng-class="{'fp-hasResults': !!searchResults}">
<div class="panel-body">
<form ng-submit="search()">
<div class="input-group has-feedback">
<span class="input-group-btn" ng-show="searchString.length > 0">
<button type="button" class="btn btn-default" ng-click="searchString=''; search()"><span class="glyphicon glyphicon-remove"></span></button>
</span>
<input type="search" class="form-control" ng-model="searchString" placeholder="Search" ng-focus="hasFocus=true" ng-blur="hasFocus=false">
<span class="input-group-btn">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</span>
<div class="form-group">
<div class="input-group">
<div class="has-feedback">
<input type="search" class="form-control" ng-model="searchString" placeholder="Search" tabindex="1">
<a href="javascript:" class="reset-button form-control-feedback" ng-click="searchString=''; search()" ng-show="searchString.length > 0"><span class="icon-clear"></span></a>
</div>
<span class="input-group-btn">
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search" tabindex="2"></span></button>
<button type="button" class="btn btn-default" ng-click="showRoutingForm()" tabindex="3"><span class="glyphicon glyphicon-road"></span></button>
</span>
</div>
</div>
</form>
<div class="fp-search-results" ng-show="searchResults">
<div uib-alert class="alert-danger no-results" ng-if="searchResults.length == 0">No results have been found.</div>
<ul class="list-group" ng-if="searchResults.length > 0">

Wyświetl plik

@ -1,6 +1,6 @@
(function(fp, $, ng, undefined) {
fp.app.factory("fpMapSearch", function($rootScope, $templateCache, $compile, fpUtils, L, $timeout) {
fp.app.factory("fpMapSearch", function($rootScope, $templateCache, $compile, fpUtils, L, $timeout, $q, fpMapSearchRoute) {
return function(map) {
var iconSuffix = ".n.32.png";
@ -54,6 +54,11 @@
map.map.flyToBounds(layerGroup.getBounds());
};
scope.showRoutingForm = function() {
searchUi.hide();
routeUi.show();
};
scope.$watch("showAll", function() {
if(!scope.searchResults)
return;
@ -150,7 +155,15 @@
}); });
}
var fpMapSearch = {
var searchUi = {
show: function() {
el.show();
},
hide: function() {
el.hide();
},
search: function(query) {
if(query != null)
scope.searchString = query;
@ -162,7 +175,10 @@
console.log("showFiles", files);
}
};
return fpMapSearch;
var routeUi = fpMapSearchRoute(map, searchUi);
return searchUi;
};
});

Wyświetl plik

@ -85,6 +85,7 @@
switch(mode) {
case "fastest":
case "shortest":
case "car":
return "by car";
case "bicycle":
return "by bicycle";
@ -251,4 +252,31 @@
};
});
fp.app.filter('fpNumberArray', function() {
return function(value, key) {
var ret = [ ];
for(var i=0; i<value; i++)
ret.push(i);
return ret;
};
});
fp.app.filter('fpRound', function(fpUtils) {
return function(value, key) {
return fpUtils.round(value, key);
};
});
fp.app.filter('fpFormatTime', function(fpUtils) {
return function(value, key) {
return fpUtils.formatTime(value);
};
});
fp.app.filter('fpRoutingMode', function(fpUtils) {
return function(value) {
return fpUtils.routingMode(value);
};
});
})(FacilPad, jQuery, angular);

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,56 @@
@font-face {
font-family: 'fontello';
src: url('fontello.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?75993818#fontello') format('svg');
}
}
*/
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-bicycle:before { content: '\f117'; } /* '' */
.icon-car:before { content: '\f125'; } /* '' */
.icon-clear:before { content: '\f135'; } /* '' */
.icon-walk:before { content: '\f216'; } /* '' */

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -21,7 +21,8 @@ var img64 = require("./gulpfile-img64");
var files = [
"app/**/*.js",
"app/**/*.css",
"app/**/*.html"
"app/**/*.html",
"assets/**/*.css"
];
gulp.task("default", [ "index" ]);

Wyświetl plik

@ -15,18 +15,26 @@ var RESOLUTION_20 = 0.0000013411044763239684 * 4;
var ROUTING_TYPES = {
fastest: "driving",
shortest: "driving",
car: "driving",
bicycle: "cycling",
pedestrian: "walking"
};
var ACCESS_TOKEN = "pk.eyJ1IjoiY2RhdXRoIiwiYSI6ImNpdTYwMmZwMDAwM3AyenBhemM5NHM4ZmgifQ.93z6yuzcsxt3eZk9NxPGHA";
function calculateRouting(points, mode) {
function calculateRouting(points, mode, simple, multiple) {
var coords = [ ];
for(var i=0; i<points.length; i++)
coords.push(points[i].lon + "," + points[i].lat);
var url = ROUTING_URL + "/" + ROUTING_TYPES[mode] + "/" + coords.join(";") + "?alternatives=true&steps=false&geometries=geojson&overview=full&annotations=false&access_token=" + encodeURIComponent(ACCESS_TOKEN);
var url = ROUTING_URL + "/" + ROUTING_TYPES[mode] + "/" + coords.join(";")
+ "?alternatives=" + (multiple ? "true" : "false")
+ "&steps=false"
+ "&geometries=geojson"
+ "&overview=" + (simple ? "simplified" : "full")
+ "&annotations=false"
+ "&access_token=" + encodeURIComponent(ACCESS_TOKEN);
return request.get({
url: url,
json: true,
@ -40,15 +48,19 @@ function calculateRouting(points, mode) {
if(body.code != 'Ok')
throw "Route could not be calculated (" + body.code + ").";
var ret = {
trackPoints : body.routes[0].geometry.coordinates.map(function(it) { return { lat: it[1], lon: it[0] }; }),
distance: body.routes[0].distance/1000,
time: body.routes[0].duration
};
var ret = [ ];
for(var i=0; i<(multiple ? body.routes.length : 1); i++) {
ret[i] = {
trackPoints : body.routes[i].geometry.coordinates.map(function(it) { return { lat: it[1], lon: it[0] }; }),
distance: body.routes[i].distance/1000,
time: body.routes[i].duration
};
_calculateZoomLevels(ret.trackPoints);
if(!simple)
_calculateZoomLevels(ret[i].trackPoints);
}
return ret;
return multiple ? ret : ret[0];
});
}

Wyświetl plik

@ -277,6 +277,15 @@ var appP = Promise.denodeify(app.listen.bind(app))(config.port, config.host).the
return search.find(data.query);
});
},
getRoutes: function(data) {
return Promise.resolve().then(function() {
if(!utils.stripObject(data, { destinations: [ { lat: "number", lon: "number" } ], mode: "string" }))
throw "Invalid parameters.";
return routing.calculateRouting(data.destinations, data.mode, true, true);
});
}
/*copyPad : function(data, callback) {