diff --git a/contribs/gmf/apps/desktop_alt/index.html b/contribs/gmf/apps/desktop_alt/index.html
index c1ad2618c072..b5c7b6632ad5 100644
--- a/contribs/gmf/apps/desktop_alt/index.html
+++ b/contribs/gmf/apps/desktop_alt/index.html
@@ -86,6 +86,10 @@
data-toggle="tooltip" data-placement="left" data-original-title="{{'Import Layer'|translate}}">
+
@@ -228,6 +232,17 @@
+
@@ -398,6 +413,19 @@
module.constant('gmfTreeUrl', 'https://geomapfish-demo.camptocamp.com/2.3/wsgi/themes?version=2&background=background&interface=desktop');
module.constant('gmfLayersUrl', 'https://geomapfish-demo.camptocamp.com/2.3/wsgi/layers/');
module.constant('gmfShortenerCreateUrl', '');
+ module.constant('ngeoRoutingOptions', {
+ 'backendUrl': 'http://routing.osm.ch/',
+ 'profiles': [
+ {label : 'Car', profile: 'routed-car'},
+ {label : 'Bike (City)', profile: 'routed-bike'},
+ {label : 'Bike (Touring)', profile: 'routed-bike-touring'},
+ {label : 'Foot', profile: 'routed-foot'},
+ {label : 'Hiking', profile: 'routed-hiking'}
+ ]
+ });
+ module.constant('ngeoNominatimSearchDefaultParams', {
+ 'countrycodes': 'CH'
+ });
module.constant('gmfSearchGroups', ['osm','district']);
// Requires that the gmfSearchGroups is specified
module.constant('gmfSearchActions', [
diff --git a/contribs/gmf/apps/desktop_alt/js/controller.js b/contribs/gmf/apps/desktop_alt/js/controller.js
index daf8e2fdcaac..d8f2be7498f3 100644
--- a/contribs/gmf/apps/desktop_alt/js/controller.js
+++ b/contribs/gmf/apps/desktop_alt/js/controller.js
@@ -11,6 +11,7 @@ goog.require('gmf.controllers.AbstractDesktopController');
goog.require('gmf.import.module');
goog.require('ngeo.googlestreetview.module');
goog.require('ngeo.statemanager.WfsPermalink');
+goog.require('ngeo.routing.module');
goog.require('ngeo.proj.EPSG2056');
goog.require('ngeo.proj.EPSG21781');
goog.require('ol');
@@ -20,6 +21,7 @@ app.desktop_alt.module = angular.module('AppDesktopAlt', [
app.module.name,
gmf.controllers.AbstractDesktopController.module.name,
gmf.import.module.name,
+ ngeo.routing.module.name,
ngeo.googlestreetview.module.name,
ngeo.statemanager.WfsPermalink.module.name,
]);
diff --git a/contribs/gmf/less/desktop.less b/contribs/gmf/less/desktop.less
index ae96629a9ac1..569fadd8f8eb 100644
--- a/contribs/gmf/less/desktop.less
+++ b/contribs/gmf/less/desktop.less
@@ -19,6 +19,7 @@
@import 'fullscreenpopup.less';
@import 'print.less';
@import 'importdatasource.less';
+@import 'routing.less';
@map-tools-size: 3rem;
@button-size: 4rem;
diff --git a/contribs/gmf/less/routing.less b/contribs/gmf/less/routing.less
new file mode 100644
index 000000000000..09de866c113c
--- /dev/null
+++ b/contribs/gmf/less/routing.less
@@ -0,0 +1,48 @@
+.ngeo-routing-error {
+ color: darkred;
+}
+
+.ngeo-routing-start .fa-map-marker {
+ color: #6BE62E;
+ text-shadow: -1px 0 #4CB01E, 0 1px #4CB01E, 1px 0 #4CB01E, 0 -1px #4CB01E;
+}
+
+.ngeo-routing-destination .fa-map-marker {
+ color: #FF3E13;
+ text-shadow: -1px 0 #CD3412, 0 1px #CD3412, 1px 0 #CD3412, 0 -1px #CD3412;
+}
+
+.ngeo-routing-vias .fa-map-marker {
+ color: #767676;
+ text-shadow: -1px 0 #000000, 0 1px #000000, 1px 0 #000000, 0 -1px #000000;
+}
+
+.ngeo-nominatim-input {
+ height: 30px;
+}
+
+/**
+ * Typeahead
+ */
+.tt-menu {
+ background-color: #fff;
+ border: 1px solid #ccc;
+}
+
+.tt-suggestion.tt-is-under-cursor, .tt-suggestion:hover {
+ color: #fff;
+ background-color: #0097cf;
+}
+
+.tt-suggestion p {
+ margin: 0;
+}
+
+.ngeo-nominatim-input .twitter-typeahead {
+ width: 100%;
+ background: #fff;
+}
+
+.ngeo-routing-vias .form-inline .input-group .form-control {
+ width: 185px;
+}
diff --git a/contribs/gmf/src/controllers/AbstractDesktopController.js b/contribs/gmf/src/controllers/AbstractDesktopController.js
index 951ab7cf1118..cf9cd14f1bfc 100644
--- a/contribs/gmf/src/controllers/AbstractDesktopController.js
+++ b/contribs/gmf/src/controllers/AbstractDesktopController.js
@@ -108,6 +108,12 @@ gmf.controllers.AbstractDesktopController = function(config, $scope, $injector)
*/
this.editFeatureActive = false;
+ /**
+ * @type {boolean}
+ * @export
+ */
+ this.routingfeatureActive = false;
+
/**
* @type {boolean}
* @export
diff --git a/examples/routing.html b/examples/routing.html
new file mode 100644
index 000000000000..f1b03e6f94a4
--- /dev/null
+++ b/examples/routing.html
@@ -0,0 +1,94 @@
+
+
+
+
Routing example
+
+
+
+
+
+
+
+
+
+
+
+
This example shows how to use ngeo's routing component (ngeo-routingComponent
)..
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/routing.js b/examples/routing.js
new file mode 100644
index 000000000000..bec547bf67ca
--- /dev/null
+++ b/examples/routing.js
@@ -0,0 +1,46 @@
+/**
+ * This example shows the ngeo routing directive.
+ */
+
+goog.provide('app.routing');
+
+goog.require('ol.Map');
+goog.require('ol.View');
+goog.require('ol.layer.Tile');
+goog.require('ol.source.OSM');
+
+/** @type {!angular.Module} */
+app.module = angular.module('app', ['ngeo']);
+
+
+/**
+ * The application's main directive.
+ * @constructor
+ * @ngInject
+ */
+app.MainController = function() {
+
+ /**
+ * @type {ol.Map}
+ * @export
+ */
+ this.map = new ol.Map({
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ })
+ ],
+ view: new ol.View({
+ center: [931010.1535989442, 5961705.842297254],
+ zoom: 9
+ })
+ });
+
+ /**
+ * @type {boolean}
+ * @export
+ */
+ this.routingfeatureActive = true;
+};
+
+app.module.controller('MainController', app.MainController);
diff --git a/externs/typeahead.js b/externs/typeahead.js
index b5ee9911bcbc..d13e4587b2ea 100644
--- a/externs/typeahead.js
+++ b/externs/typeahead.js
@@ -139,7 +139,7 @@ let TypeaheadTemplates;
/**
* @typedef {{
- * source: function(string,function(Array.
)),
+ * source: function(string,function(Array.),(function(Array.)|undefined)),
* async: (boolean|undefined),
* name: (string|undefined),
* limit: (number|undefined),
diff --git a/options/ngeox.js b/options/ngeox.js
index 8592dc673f3e..864b581bc760 100644
--- a/options/ngeox.js
+++ b/options/ngeox.js
@@ -2605,3 +2605,55 @@ ngeox.CreatePrint;
* }}
*/
ngeox.FilterCondition;
+
+
+/**
+ * Format a duration in seconds to a more readable form.
+ * Arguments:
+ * - duration The duration in seconds.
+ * @typedef {function(number): string}
+ */
+ngeox.duration;
+
+
+
+/**
+ * @typedef {{
+ * name: (string),
+ * coordinate: (ol.Coordinate)
+ * }}
+ */
+ngeox.NominatimSearchResult;
+
+/**
+ * @typedef {{
+ * display_name: (string),
+ * lon: (number),
+ * lat: (number)
+ * }}
+ */
+ngeox.NominatimSearchResponseResult;
+
+/**
+ * @typedef {{
+ * feature: (?ol.Feature),
+ * onSelect: (function(ngeox.NominatimSearchResult))
+ * }}
+ */
+ngeox.RoutingVia;
+
+/**
+ * @typedef {{
+ * label: (string),
+ * profile: (string)
+ * }}
+ */
+ngeox.RoutingProfile;
+
+/**
+ * @typedef {{
+ * backendUrl: (string|undefined),
+ * profiles: (Array.|undefined)
+ * }}
+ */
+ngeox.RoutingOptions;
diff --git a/src/misc/filters.js b/src/misc/filters.js
index 6864c09cb608..2f2e1c4b8988 100644
--- a/src/misc/filters.js
+++ b/src/misc/filters.js
@@ -321,3 +321,108 @@ ngeo.misc.filters.trustHtmlFilter = function($sce) {
};
ngeo.misc.filters.filter('ngeoTrustHtml', ngeo.misc.filters.trustHtmlFilter);
+
+
+/**
+ * A filter used to format a time duration in seconds into a more
+ * readable form.
+ * Only the two largest units will be shown.
+ *
+ * Examples:
+ * {{42 | ngeoDuration}} => 42 seconds
+ * {{132 | ngeoDuration}} => 2 minutes 12 seconds
+ * {{3910 | ngeoDuration}} => 1 hour 5 minutes
+ * -> Note: the remaining 10 seconds will be dropped
+ *
+ * @param {angularGettext.Catalog} gettextCatalog Gettext catalog.
+ * @return {ngeox.duration} Function used to format a time duration
+ * in seconds into a string.
+ * @ngInject
+ * @ngdoc filter
+ * @ngname ngeoDuration
+ */
+ngeo.misc.filters.Duration = function(gettextCatalog) {
+ // time unit enum
+ const TimeUnits = Object.freeze({
+ SECONDS: Symbol('seconds'),
+ MINUTES: Symbol('minutes'),
+ HOURS: Symbol('hours'),
+ DAYS: Symbol('days')
+ });
+
+ /**
+ * @param {number} amount Amount of time.
+ * @param {symbol} unit Unit of time.
+ * @return {string} formatted and translated string
+ */
+ const pluralize = function(amount, unit) {
+ let formattedUnit = '';
+ switch (unit) {
+ case TimeUnits.SECONDS:
+ formattedUnit = amount !== 1 ? gettextCatalog.getString('seconds') : gettextCatalog.getString('second');
+ break;
+ case TimeUnits.MINUTES:
+ formattedUnit = amount !== 1 ? gettextCatalog.getString('minutes') : gettextCatalog.getString('minute');
+ break;
+ case TimeUnits.HOURS:
+ formattedUnit = amount !== 1 ? gettextCatalog.getString('hours') : gettextCatalog.getString('hour');
+ break;
+ case TimeUnits.DAYS:
+ formattedUnit = amount !== 1 ? gettextCatalog.getString('days') : gettextCatalog.getString('day');
+ break;
+ default:
+ break;
+ }
+ return `${amount} ${formattedUnit}`;
+ };
+
+ /**
+ * @param {number} duration The duration in seconds.
+ * @return {string} The formatted string.
+ */
+ const result = function(duration) {
+ // round to next integer
+ duration = Math.round(duration);
+
+ // just seconds
+ let output;
+ if (duration < 60) {
+ return pluralize(duration, TimeUnits.SECONDS);
+ }
+
+ // minutes (+ seconds)
+ let remainder = duration % 60; // seconds
+ duration = Math.floor(duration / 60); // minutes
+ if (duration < 60) { // less than an hour
+ output = pluralize(duration, TimeUnits.MINUTES);
+ if (remainder > 0) {
+ output += ` ${pluralize(remainder, TimeUnits.SECONDS)}`;
+ }
+ return output;
+ }
+
+ // hours (+ minutes)
+ remainder = duration % 60; // minutes
+ duration = Math.floor(duration / 60); // hours
+ if (duration < 24) { // less than a day
+ output = pluralize(duration, TimeUnits.HOURS);
+ if (remainder > 0) {
+ output += ` ${pluralize(remainder, TimeUnits.MINUTES)}`;
+ }
+ return output;
+ }
+
+ // days (+ hours)
+ remainder = duration % 24; // hours
+ duration = Math.floor(duration / 24); // days
+ output = pluralize(duration, TimeUnits.DAYS);
+ if (remainder > 0) {
+ output += ` ${pluralize(remainder, TimeUnits.HOURS)}`;
+ }
+ return output;
+ };
+
+ return result;
+};
+
+ngeo.misc.filters.filter('ngeoDuration', ngeo.misc.filters.Duration);
diff --git a/src/routing/NominatimInputComponent.js b/src/routing/NominatimInputComponent.js
new file mode 100644
index 000000000000..5154db8c2b59
--- /dev/null
+++ b/src/routing/NominatimInputComponent.js
@@ -0,0 +1,167 @@
+goog.provide('ngeo.routing.NominatimInputComponent');
+
+goog.require('ngeo.search.searchDirective');
+goog.require('ngeo.routing.NominatimService');
+
+
+/**
+ * @type {!angular.Module}
+ */
+ngeo.routing.NominatimInputComponent.module = angular.module('ngeoRoutingNominatimInputComponent', [
+ ngeo.search.searchDirective.module.name,
+ ngeo.routing.NominatimService.module.name
+]);
+
+// webpack: exports.run(/* @ngInject */ ($templateCache) => {
+// webpack: $templateCache.put('ngeo/routing/nominatiminput', require('./nominatiminput.html'));
+// webpack: });
+
+
+ngeo.routing.NominatimInputComponent.module.value('ngeoRoutingNominatimInputComponentTemplateUrl',
+ /**
+ * @param {!angular.Attributes} $attrs Attributes.
+ * @return {string} Template URL.
+ */
+ ($attrs) => {
+ const templateUrl = $attrs['ngeoRoutingNominatimInputComponentTemplateUrl'];
+ return templateUrl !== undefined ? templateUrl :
+ `${ngeo.baseModuleTemplateUrl}/routing/nominatiminput.html`; // nowebpack
+ // webpack: 'ngeo/routing/nominatiminput';
+ }
+);
+
+
+/**
+ * @param {!angular.Attributes} $attrs Attributes.
+ * @param {!function(!angular.Attributes): string} ngeoRoutingNominatimInputComponentTemplateUrl Template function.
+ * @return {string} Template URL.
+ * @ngInject
+ */
+function ngeoRoutingNominatimInputComponentTemplateUrl($attrs, ngeoRoutingNominatimInputComponentTemplateUrl) {
+ return ngeoRoutingNominatimInputComponentTemplateUrl($attrs);
+}
+
+
+/**
+ * @param {!angular.JQLite} $element Element.
+ * @param {angular.$injector} $injector Main injector.
+ * @param {!angular.Scope} $scope Scope.
+ * @param {!ngeo.NominatimService} ngeoNominatimService service for Nominatim
+ * @constructor
+ * @private
+ * @ngInject
+ * @ngdoc controller
+ * @ngname NgeoNominatimInputController
+ */
+ngeo.NgeoNominatimInputController = function($element, $injector, $scope, ngeoNominatimService) {
+
+ /**
+ * @type {!angular.JQLite}
+ * @private
+ */
+ this.element_ = $element;
+
+ /**
+ * @type {angular.Scope}
+ * @private
+ */
+ this.$scope_ = $scope;
+
+ /**
+ * @type {ngeo.NominatimService}
+ * @export
+ */
+ this.ngeoNominatimService = ngeoNominatimService;
+
+ /**
+ * @type {(function(Object)|undefined)}
+ * @export
+ */
+ this.onSelect;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.inputValue;
+
+ /**
+ * @type {TypeaheadOptions}
+ * @export
+ */
+ this.options = /** @type {TypeaheadOptions} */ ({
+ });
+
+ /**
+ * @type {Array.}
+ * @export
+ */
+ this.datasets = [/** @type {TypeaheadDataset} */({
+ name: 'nominatim',
+ display: 'name',
+ source: this.ngeoNominatimService.typeaheadSourceDebounced
+ })];
+
+ /**
+ * @type {ngeox.SearchDirectiveListeners}
+ * @export
+ */
+ this.listeners = /** @type {ngeox.SearchDirectiveListeners} */({
+ select: this.select_.bind(this)
+ });
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.placeholder = '';
+
+};
+
+/**
+ * @param {jQuery.Event} event Event.
+ * @param {ngeox.NominatimSearchResult} suggestion Suggestion.
+ * @param {TypeaheadDataset} dataset Dataset.
+ * @this {ngeo.NgeoNominatimInputController}
+ * @private
+ */
+ngeo.NgeoNominatimInputController.prototype.select_ = function(event, suggestion, dataset) {
+ if (this.onSelect) {
+ this.onSelect(suggestion);
+ }
+};
+
+/**
+ * Input form field which provides Nominatim typeahead lookup using {@link ngeo.NominatimService}.
+ *
+ * Example:
+ *
+ *
+ *
+ * Is used in in the partial of {@link ngeo.routingFeatureComponent}.
+ *
+ * See the [../examples/routing.html](../examples/routing.html) example to see it in action.
+ *
+ * @htmlAttribute {function(ngeox.NominatimSearchResult)} ngeo-nominatim-input-on-select
+ * Event fired when user selects a new suggestion.
+ * @htmlAttribute {string} ngeo-nominatim-input-value
+ * Value of input field, will be set to the label of the search result.
+ * @htmlAttribute {string} ngeo-nominatim-input-placeholder
+ * Placeholder text, when field is empty.
+ * @ngdoc directive
+ * @ngname ngeoNominatimInput
+ */
+ngeo.routing.NominatimInputComponent.component_ = {
+ controller: ngeo.NgeoNominatimInputController,
+ bindings: {
+ 'onSelect': '=?ngeoNominatimInputOnSelect',
+ 'inputValue': '=?ngeoNominatimInputValue',
+ 'placeholder': '@?ngeoNominatimInputPlaceholder'
+ },
+ templateUrl: ngeoRoutingNominatimInputComponentTemplateUrl
+};
+
+ngeo.routing.NominatimInputComponent.module.component('ngeoNominatimInput', ngeo.routing.NominatimInputComponent.component_);
diff --git a/src/routing/NominatimService.js b/src/routing/NominatimService.js
new file mode 100644
index 000000000000..26b2ed555b09
--- /dev/null
+++ b/src/routing/NominatimService.js
@@ -0,0 +1,176 @@
+goog.provide('ngeo.routing.NominatimService');
+
+goog.require('ngeo.misc.debounce');
+
+
+/**
+ * Service to provide access to Nominatim, which allows to search for
+ * OSM data by name and address.
+ * @param {angular.$http} $http Angular http service.
+ * @param {angular.$injector} $injector Main injector.
+ * @param {ngeo.Debounce} ngeoDebounce ngeo Debounce service.
+ * @constructor
+ * @struct
+ * @ngdoc service
+ * @ngInject
+ * @export
+ * @ngname ngeoNominatimService
+ * @see https://wiki.openstreetmap.org/wiki/Nominatim
+ */
+ngeo.routing.NominatimService = function($http, $injector, ngeoDebounce) {
+
+ /**
+ * @type {angular.$http}
+ * @private
+ */
+ this.$http_ = $http;
+
+ /**
+ * @type {ngeo.Debounce}
+ * @private
+ */
+ this.ngeoDebounce_ = ngeoDebounce;
+
+ /**
+ * URL for Nominatim backend
+ * Defaults openstreetmap instance.
+ * @type {string}
+ * @private
+ */
+ this.nominatimUrl_ = 'http://nominatim.openstreetmap.org/';
+
+ if ($injector.has('ngeoNominatimUrl')) {
+ this.nominatimUrl_ = $injector.get('ngeoNominatimUrl');
+
+ // the url is expected to end with a slash
+ if (this.nominatimUrl_.substr(-1) !== '/') {
+ this.nominatimUrl_ += '/';
+ }
+ }
+
+ /**
+ * @type {Object}
+ * @private
+ */
+ this.searchDefaultParams_ = {};
+
+ if ($injector.has('ngeoNominatimSearchDefaultParams')) {
+ this.searchDefaultParams_ = $injector.get('ngeoNominatimSearchDefaultParams');
+ }
+
+ /**
+ * Delay (in milliseconds) to avoid calling the API too often.
+ * Only if there were no calls for that many milliseconds,
+ * the last call will be executed.
+ * @type {number}
+ * @private
+ */
+ this.typeaheadDebounceDelay_ = 500;
+
+ /**
+ * @export
+ * @type {function(string,function(Array.),(function(Array.)|undefined))}
+ */
+ this.typeaheadSourceDebounced =
+ /** @type{function(string,function(Array.),(function(Array.)|undefined))} */
+ (this.ngeoDebounce_(/** @type {function(?)} */ (this.typeaheadSource_.bind(this)), this.typeaheadDebounceDelay_, true));
+};
+
+/**
+ * Search by name
+ * @param {string} query Search query
+ * @param {?Object} params Optional parameters
+ * @return {!angular.$http.HttpPromise} promise of the Nominatim API request
+ * @see https://wiki.openstreetmap.org/wiki/Nominatim#Search
+ * @export
+ */
+ngeo.routing.NominatimService.prototype.search = function(query, params) {
+ let url = `${this.nominatimUrl_}search?q=${query}`;
+
+ params = params || {};
+ params = Object.assign({}, this.searchDefaultParams_, params);
+
+ // require JSON response
+ params['format'] = 'json';
+
+ if (params) {
+ url += '&';
+ const options = [];
+ for (const option of Object.keys(params)) {
+ options.push(`${option}=${params[option]}`);
+ }
+ url += options.join('&');
+ }
+
+ return this.$http_.get(url);
+};
+
+/**
+ * Reverse Geocoding
+ * @param {ol.Coordinate} coordinate Search coordinate in LonLat projection
+ * @param {(Object|undefined)} params Optional parameters
+ * @return {!angular.$http.HttpPromise} promise of the Nominatim API request
+ * @see https://wiki.openstreetmap.org/wiki/Nominatim#Reverse_Geocoding
+ * @export
+ */
+ngeo.routing.NominatimService.prototype.reverse = function(coordinate, params) {
+ let url = `${this.nominatimUrl_}reverse`;
+
+ params = Object.assign({}, params);
+
+ // coordinate
+ params['lon'] = coordinate[0];
+ params['lat'] = coordinate[1];
+
+ // require JSON response
+ params['format'] = 'json';
+
+ if (params) {
+ url += '?';
+ const options = [];
+ for (const option of Object.keys(params)) {
+ options.push(`${option}=${params[option]}`);
+ }
+ url += options.join('&');
+ }
+
+ return this.$http_.get(url);
+};
+
+/**
+ * @param {string} query Search query
+ * @param {function(Array.)} syncResults Callback for synchronous execution, unused
+ * @param {function(Array.)} asyncResults Callback for asynchronous execution
+ * @private
+ */
+ngeo.routing.NominatimService.prototype.typeaheadSource_ = function(query, syncResults, asyncResults) {
+ const onSuccess_ = function(resp) {
+ /**
+ * Parses result response.
+ * @param {ngeox.NominatimSearchResponseResult} result Result
+ * @return {ngeox.NominatimSearchResult} Parsed result
+ */
+ const parse = function(result) {
+ return /** @type{ngeox.NominatimSearchResult} */({
+ coordinate: [result.lon, result.lat],
+ name: result.display_name
+ });
+ };
+ asyncResults(resp.data.map(parse));
+ };
+
+ const onError_ = function(resp) {
+ asyncResults([]);
+ };
+
+ this.search(query, {}).then(onSuccess_, onError_);
+};
+
+/**
+ * @type {!angular.Module}
+ */
+ngeo.routing.NominatimService.module = angular.module('ngeoNominatimService', [
+ ngeo.misc.debounce.name
+]);
+
+ngeo.routing.NominatimService.module.service('ngeoNominatimService', ngeo.routing.NominatimService);
diff --git a/src/routing/RoutingComponent.js b/src/routing/RoutingComponent.js
new file mode 100644
index 000000000000..061a7eba517b
--- /dev/null
+++ b/src/routing/RoutingComponent.js
@@ -0,0 +1,403 @@
+goog.provide('ngeo.routing.RoutingComponent');
+
+goog.require('ngeo.misc.debounce');
+goog.require('ngeo.misc.filters');
+goog.require('ngeo.routing.NominatimService');
+goog.require('ngeo.routing.RoutingService');
+goog.require('ngeo.routing.RoutingFeatureComponent');
+goog.require('ol.format.GeoJSON');
+goog.require('ol.source.Vector');
+goog.require('ol.layer.Vector');
+goog.require('ol.style.Style');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Stroke');
+goog.require('ol.proj');
+goog.require('ol.Feature');
+goog.require('ol.geom.LineString');
+
+ngeo.routing.RoutingComponent.module = angular.module('ngeoRoutingFeatureComponent', [
+ ngeo.misc.debounce.name,
+ ngeo.misc.filters.name,
+ ngeo.routing.NominatimService.module.name,
+ ngeo.routing.RoutingService.module.name,
+ ngeo.routing.RoutingFeatureComponent.module.name
+]);
+
+
+// webpack: exports.run(/* @ngInject */ ($templateCache) => {
+// webpack: $templateCache.put('ngeo/routing/routingfeature', require('./routingfeature.html'));
+// webpack: });
+
+
+ngeo.routing.RoutingComponent.module.value('ngeoRoutingTemplateUrl',
+ /**
+ * @param {!angular.Attributes} $attrs Attributes.
+ * @return {string} Template URL.
+ */
+ ($attrs) => {
+ const templateUrl = $attrs['ngeoRoutingTemplateUrl'];
+ return templateUrl !== undefined ? templateUrl :
+ `${ngeo.baseModuleTemplateUrl}/routing/routing.html`; // nowebpack
+ // webpack: 'ngeo/routing/routing';
+ }
+);
+
+
+/**
+ * @param {!angular.Attributes} $attrs Attributes.
+ * @param {!function(!angular.Attributes): string} ngeoRoutingTemplateUrl Template function.
+ * @return {string} Template URL.
+ * @ngInject
+ */
+function ngeoRoutingTemplateUrl($attrs, ngeoRoutingTemplateUrl) {
+ return ngeoRoutingTemplateUrl($attrs);
+}
+
+
+/**
+ * The controller for the routing directive.
+ * @param {angular.$injector} $injector Main injector.
+ * @param {!angular.Scope} $scope Scope.
+ * @param {!ngeo.RoutingService} ngeoRoutingService service for OSRM routing
+ * @param {!ngeo.NominatimService} ngeoNominatimService service for Nominatim
+ * @param {!angular.$q} $q Angular q service
+ * @param {ngeo.Debounce} ngeoDebounce ngeo Debounce service.
+ * @constructor
+ * @private
+ * @ngInject
+ * @ngdoc controller
+ * @ngname NgeoRoutingController
+ */
+ngeo.routing.RoutingComponent.Controller = function($injector, $scope, ngeoRoutingService, ngeoNominatimService, $q, ngeoDebounce) {
+
+ /**
+ * @type {angular.Scope}
+ * @private
+ */
+ this.$scope_ = $scope;
+
+ /**
+ * @type {ngeo.RoutingService}
+ * @private
+ */
+ this.ngeoRoutingService_ = ngeoRoutingService;
+
+ /**
+ * @type {ngeo.NominatimService}
+ * @private
+ */
+ this.ngeoNominatimService_ = ngeoNominatimService;
+
+ /**
+ * @type {ngeox.RoutingOptions}
+ * @private
+ */
+ this.routingOptions_ = $injector.has('ngeoRoutingOptions') ? $injector.get('ngeoRoutingOptions') : {};
+
+ /**
+ * Available routing profiles.
+ * Example: [
+ * {
+ * label: 'Car', // used as label in the UI
+ * profile: 'routed-car' // used as part of the query
+ * }
+ * ]
+ * @type {Array}
+ * @export
+ */
+ this.routingProfiles = this.routingOptions_.profiles || [];
+
+ /**
+ * @type {?ngeox.RoutingProfile}
+ * @export
+ */
+ this.selectedRoutingProfile = this.routingProfiles.length > 0 ? this.routingProfiles[0] : null;
+
+ $scope.$watch(
+ () => this.selectedRoutingProfile,
+ this.calculateRoute.bind(this)
+ );
+
+ /**
+ * @type {angular.$q}
+ * @private
+ */
+ this.$q_ = $q;
+
+ /**
+ * @type {ol.Map}
+ * @export
+ */
+ this.map;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.errorMessage = '';
+
+ /**
+ * @type {ol.Feature}
+ * @export
+ */
+ this.startFeature_ = null;
+
+ /**
+ * @type {ol.Feature}
+ * @export
+ */
+ this.targetFeature_ = null;
+
+ /**
+ * @type {Array.}
+ * @export
+ */
+ this.viaArray = [];
+
+ /**
+ * @type {Object}
+ * @export
+ */
+ this.colors = {
+ 'start.fill': '#6BE62E',
+ 'start.stroke': '#4CB01E',
+ 'destination.fill': '#FF3E13',
+ 'destination.stroke': '#CD3412',
+ 'via.fill': '#767676',
+ 'via.stroke': '#000000'
+ };
+
+ /**
+ * @type {ol.source.Vector}
+ * @private
+ */
+ this.routeSource_ = new ol.source.Vector({
+ features: []
+ });
+
+ /**
+ * @type {ol.layer.Vector}
+ * @private
+ */
+ this.routeLayer_ = new ol.layer.Vector({
+ source: this.routeSource_,
+ style: new ol.style.Style({
+ fill: new ol.style.Fill({
+ color: 'rgba(16, 112, 29, 0.6)'
+ }),
+ stroke: new ol.style.Stroke({
+ color: 'rgba(16, 112, 29, 0.6)',
+ width: 5
+ })
+ })
+ });
+
+ /**
+ * Distance of route in meters
+ * @type {number}
+ * @export
+ */
+ this.routeDistance = 0;
+
+ /**
+ * Duration of route in minutes.
+ * @type {?number}
+ * @export
+ */
+ this.routeDuration = null;
+
+ /**
+ * @type {RegExp}
+ * @private
+ */
+ this.regexIsFormattedCoord = /\d+\.\d+\/\d+\.\d+/;
+
+ /**
+ * @type {ol.interaction.Draw}
+ * @private
+ */
+ this.draw_ = null;
+
+ const debounceDelay = 200; // in milliseconds
+
+ /**
+ * Debounced because in some cases (reverse route) multiple changes are done
+ * at once and spam this function.
+ * @export
+ * @type {function()}
+ */
+ this.handleChange = /** @type {function()} */
+ (ngeoDebounce(
+ /** @type {function(?)} */ (this.calculateRoute.bind(this)),
+ debounceDelay,
+ true));
+};
+
+/**
+ * Init the controller
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.$onInit = function() {
+ this.map.addLayer(this.routeLayer_);
+};
+
+/**
+ * Clears start, end and vias. Removes features from map.
+ * @export
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.clearRoute = function() {
+ this.startFeature_ = null;
+ this.targetFeature_ = null;
+ this.viaArray = [];
+ this.routeDistance = 0;
+ this.routeDuration = null;
+ this.routeSource_.clear();
+ this.errorMessage = '';
+};
+
+/**
+ * Converts feature point into LonLat coordinate.
+ * @param {ol.Feature} point Feature point to convert
+ * @return {ol.Coordinate} LonLat coordinate
+ * @private
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.getLonLatFromPoint_ = function(point) {
+ const geometry = /** @type {ol.geom.Point} */ (point.getGeometry());
+ const coords = geometry.getCoordinates();
+ const projection = this.map.getView().getProjection();
+ return ol.proj.toLonLat(coords, projection);
+};
+
+/**
+ * Flip start and target and re-calculate route.
+ * @export
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.reverseRoute = function() {
+ // swap start and target
+ const tmpFeature = this.startFeature_;
+ this.startFeature_ = this.targetFeature_;
+ this.targetFeature_ = tmpFeature;
+
+ // reverse vias
+ this.viaArray = this.viaArray.reverse();
+
+ // recalculation is done by the debounced handleChange
+};
+
+/**
+ * @param {Object} route Routes of OSRM response
+ * @returns {Array} parsed route features
+ * @private
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.parseRoute_ = function(route) {
+ let parsedRoutes = [];
+ const format = new ol.format.GeoJSON();
+ const formatConfig = {
+ dataProjection: 'EPSG:4326',
+ featureProjection: this.map.getView().getProjection()
+ };
+ // if there are is useful "legs" data, parse this
+ if (route.legs) {
+ parsedRoutes = route.legs.map(leg => leg.steps.map(step => new ol.Feature({geometry: format.readGeometry(step.geometry, formatConfig)})));
+ // flatten
+ parsedRoutes = [].concat(...parsedRoutes);
+ } else if (route.geometry) {
+ // otherwise parse (overview) geometry
+ parsedRoutes.push(new ol.Feature({geometry: format.readGeometry(route.geometry, formatConfig)}));
+ }
+ return parsedRoutes;
+};
+
+/**
+ * @export
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.calculateRoute = function() {
+ if (this.startFeature_ && this.targetFeature_) {
+ // remove rendered routes
+ this.routeSource_.clear();
+
+ const coordFrom = this.getLonLatFromPoint_(this.startFeature_);
+ const coordTo = this.getLonLatFromPoint_(this.targetFeature_);
+ const vias = this.viaArray.filter(via => via.feature !== null).map(via => this.getLonLatFromPoint_(via.feature));
+ const route = [coordFrom].concat(vias, [coordTo]);
+
+ const onSuccess_ = (function(resp) {
+ const features = this.parseRoute_(resp.data.routes[0]);
+ if (features.length === 0) {
+ console.log('No route or not supported format.');
+ return;
+ }
+ this.routeSource_.addFeatures(features);
+
+ // recenter map on route
+ this.map.getView().fit(this.routeSource_.getExtent());
+
+ this.routeDistance = parseInt(resp.data.routes[0].distance, 10);
+ this.routeDuration = resp.data.routes[0].duration;
+
+ // get first and last coordinate of route
+ const startRoute = /** @type{ol.geom.LineString} */(features[0].getGeometry()).getCoordinateAt(0);
+ const endRoute = /** @type{ol.geom.LineString} */(features[features.length - 1].getGeometry()).getCoordinateAt(1);
+
+ // build geometries to connect route to start and end point of query
+ const startToRoute = [/** @type {ol.geom.Point} */(this.startFeature_.getGeometry()).getCoordinates(), startRoute];
+ const routeToEnd = [endRoute, /** @type {ol.geom.Point} */(this.targetFeature_.getGeometry()).getCoordinates()];
+ const routeConnections = [
+ new ol.Feature(new ol.geom.LineString(startToRoute)),
+ new ol.Feature(new ol.geom.LineString(routeToEnd))
+ ];
+
+ // add them to the source
+ this.routeSource_.addFeatures(routeConnections);
+ }).bind(this);
+
+ const onError_ = (function(resp) {
+ this.errorMessage = 'Error: routing server not responding.';
+ console.log(resp);
+ }).bind(this);
+
+ const options = {};
+ options['steps'] = true;
+ options['overview'] = false;
+ options['geometries'] = 'geojson';
+
+ const config = {};
+ config['options'] = options;
+
+ if (this.selectedRoutingProfile) {
+ config['instance'] = this.selectedRoutingProfile['profile'];
+ }
+
+ this.$q_.when(this.ngeoRoutingService_.getRoute(route, config))
+ .then(onSuccess_.bind(this), onError_.bind(this));
+ }
+};
+
+/**
+ * @export
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.addVia = function() {
+ this.viaArray.push(/** @type{ngeox.RoutingVia} */({
+ feature: null,
+ onSelect: null
+ }));
+};
+
+/**
+ * @param {number} index Array index.
+ * @export
+ */
+ngeo.routing.RoutingComponent.Controller.prototype.deleteVia = function(index) {
+ if (this.viaArray.length > index) {
+ this.viaArray.splice(index, 1);
+ this.calculateRoute();
+ }
+};
+
+
+ngeo.routing.RoutingComponent.module.component('ngeoRouting', {
+ controller: ngeo.routing.RoutingComponent.Controller,
+ bindings: {
+ 'map': ' {
+// webpack: $templateCache.put('ngeo/routing/routingfeature', require('./routingfeature.html'));
+// webpack: });
+
+
+ngeo.routing.RoutingFeatureComponent.module.value('ngeoRoutingFeatureTemplateUrl',
+ /**
+ * @param {!angular.Attributes} $attrs Attributes.
+ * @return {string} Template URL.
+ */
+ ($attrs) => {
+ const templateUrl = $attrs['ngeoRoutingFeatureTemplateUrl'];
+ return templateUrl !== undefined ? templateUrl :
+ `${ngeo.baseModuleTemplateUrl}/routing/routingfeature.html`; // nowebpack
+ // webpack: 'ngeo/routing/routingfeature';
+ }
+);
+
+/**
+ * @param {!angular.Attributes} $attrs Attributes.
+ * @param {!function(!angular.Attributes): string} ngeoRoutingFeatureTemplateUrl Template function.
+ * @return {string} Template URL.
+ * @ngInject
+ */
+function ngeoRoutingFeatureTemplateUrl($attrs, ngeoRoutingFeatureTemplateUrl) {
+ return ngeoRoutingFeatureTemplateUrl($attrs);
+}
+
+
+/**
+ * @param {!angular.Scope} $scope Angular scope.
+ * @param {angular.$timeout} $timeout Angular timeout service.
+ * @param {!angular.$q} $q Angular q service
+ * @param {!ngeo.NominatimService} ngeoNominatimService service for Nominatim
+ * @constructor
+ * @private
+ * @ngInject
+ * @ngdoc controller
+ * @ngname NgeoRoutingFeatureController
+ */
+ngeo.routing.RoutingFeatureComponent.Controller = function($scope, $timeout, $q, ngeoNominatimService) {
+
+ /**
+ * @type {!angular.Scope}
+ * @private
+ */
+ this.scope_ = $scope;
+
+ /**
+ * @type {angular.$timeout}
+ * @private
+ */
+ this.timeout_ = $timeout;
+
+ /**
+ * @type {angular.$q}
+ * @private
+ */
+ this.$q_ = $q;
+
+ /**
+ * @type {ngeo.NominatimService}
+ * @private
+ */
+ this.ngeoNominatimService_ = ngeoNominatimService;
+
+ /**
+ * @type {ol.Map}
+ * @private
+ */
+ this.map;
+
+ /**
+ * @type {ol.Feature}
+ * @export
+ */
+ this.feature;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.featureLabel = '';
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.fillColor;
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.strokeColor;
+
+ /**
+ * @type {function(ol.Feature)}
+ * @export
+ */
+ this.onChange;
+
+ /**
+ * @type {ol.Collection}
+ * @private
+ */
+ this.vectorFeatures_ = new ol.Collection();
+
+ /**
+ * @type {ol.source.Vector}
+ * @private
+ */
+ this.vectorSource_ = new ol.source.Vector({
+ features: this.vectorFeatures_
+ });
+
+ /**
+ * @type {ol.layer.Vector}
+ * @private
+ */
+ this.vectorLayer_ = new ol.layer.Vector({
+ source: this.vectorSource_,
+ style: (function(feature, resolution) {
+ return [new ol.style.Style({
+ text: new ol.style.Text({
+ fill: new ol.style.Fill({
+ color: this.fillColor || '#000000'
+ }),
+ font: 'normal 30px FontAwesome',
+ offsetY: -15,
+ stroke: new ol.style.Stroke({
+ width: 3,
+ color: this.strokeColor || '#000000'
+ }),
+ text: '\uf041'
+ })
+ })];
+ }).bind(this)
+ });
+
+ /**
+ * Interaction for moving start and end.
+ * @type {ol.interaction.Modify}
+ * @private
+ */
+ this.modifyFeature_ = new ol.interaction.Modify({
+ features: this.vectorFeatures_
+ });
+
+ /**
+ * @type {ol.interaction.Draw}
+ * @private
+ */
+ this.draw_ = null;
+
+ /**
+ * @param {ngeox.NominatimSearchResult} selected Selected result.
+ * @export
+ */
+ this.onSelect = this.onSelect_.bind(this);
+
+ /**
+ * @type {string}
+ * @export
+ */
+ this.errorMessage = '';
+};
+
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.$onInit = function() {
+ this.map.addLayer(this.vectorLayer_);
+
+ // setup modify interaction
+ this.modifyFeature_.setActive(true);
+ this.map.addInteraction(this.modifyFeature_);
+
+ this.modifyFeature_.on('modifyend', (event) => {
+ const feature = event.features.getArray()[0];
+ this.vectorSource_.clear();
+ this.snapFeature_(feature);
+ });
+
+ this.scope_.$watch(
+ () => this.feature,
+ (newVal, oldVal) => {
+ if (newVal) {
+ this.onFeatureChange_();
+ }
+ if (newVal === null) {
+ this.vectorSource_.clear();
+ this.featureLabel = '';
+ }
+ }
+ );
+};
+
+/**
+ * Cleanup, mostly relevant for vias.
+ */
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.$onDestroy = function() {
+ this.map.removeLayer(this.vectorLayer_);
+ this.modifyFeature_.setActive(false);
+ this.map.removeInteraction(this.modifyFeature_);
+};
+
+/**
+ * @export
+ */
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.set = function() {
+ if (this.draw_) {
+ this.map.removeInteraction(this.draw_);
+ }
+
+ this.draw_ = new ol.interaction.Draw({
+ features: this.vectorFeatures_,
+ type: /** @type {ol.geom.GeometryType} */ ('Point')
+ });
+
+ this.draw_.on('drawstart', () => {
+ if (this.feature) {
+ this.vectorSource_.removeFeature(this.feature);
+ }
+ });
+
+ this.draw_.on('drawend', (event) => {
+ if (this.draw_) {
+ this.map.removeInteraction(this.draw_);
+ }
+ this.snapFeature_(event.feature);
+ this.modifyFeature_.setActive(true);
+ });
+
+ this.modifyFeature_.setActive(false);
+ this.map.addInteraction(this.draw_);
+};
+
+/**
+ * @param {ol.Coordinate} coordinate LonLat coordinate.
+ * @param {string} label Feature name/label.
+ * @private
+ */
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.setFeature_ = function(coordinate, label) {
+ const transformedCoordinate = ol.proj.fromLonLat(coordinate, this.map.getView().getProjection());
+ if (label === '') {
+ label = transformedCoordinate.join('/');
+ }
+ this.feature = new ol.Feature({
+ geometry: new ol.geom.Point(transformedCoordinate),
+ name: label
+ });
+};
+
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.onFeatureChange_ = function() {
+ // update label
+ this.featureLabel = /** @type{string} */(this.feature.get('name') || '');
+
+ //update vector source
+ this.vectorSource_.clear();
+ this.vectorSource_.addFeature(this.feature);
+
+ // notify others
+ if (this.onChange) {
+ this.timeout_(() => {
+ this.onChange(this.feature);
+ });
+ }
+};
+
+/**
+ * @param {ngeox.NominatimSearchResult} selected Selected result.
+ * @private
+ */
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.onSelect_ = function(selected) {
+ const coordinate = selected.coordinate.map(parseFloat);
+ const label = selected.label;
+ this.setFeature_(coordinate, label);
+ const newCoordinates = /** @type{ol.geom.Point} */(this.feature.getGeometry()).getCoordinates();
+ this.map.getView().setCenter(newCoordinates);
+};
+
+/**
+ * Snaps a feature to the street network using the getNearest
+ * function of the routing service. Replaces the feature.
+ * @param {ol.Feature} feature Feature to snap
+ * @private
+ */
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.snapFeature_ = function(feature) {
+ const coord = this.getLonLatFromPoint_(feature);
+ const config = {};
+
+ const onSuccess = (function(resp) {
+ const lon = parseFloat(resp['data']['lon']);
+ const lat = parseFloat(resp['data']['lat']);
+ const coordinate = [lon, lat];
+ const label = resp['data']['display_name'];
+ this.setFeature_(coordinate, label);
+ }).bind(this);
+
+ const onError = (function(resp) {
+ this.errorMessage = 'Error: nominatim server not responding.';
+ console.log(resp);
+ }).bind(this);
+
+ this.$q_.when(this.ngeoNominatimService_.reverse(coord, config))
+ .then(onSuccess.bind(this), onError.bind(this));
+};
+
+/**
+ * Converts feature point into LonLat coordinate.
+ * @param {ol.Feature} point Feature point to convert
+ * @return {ol.Coordinate} LonLat coordinate
+ * @private
+ */
+ngeo.routing.RoutingFeatureComponent.Controller.prototype.getLonLatFromPoint_ = function(point) {
+ const geometry = /** @type {ol.geom.Point} */ (point.getGeometry());
+ const coords = geometry.getCoordinates();
+ const projection = this.map.getView().getProjection();
+ return ol.proj.toLonLat(coords, projection);
+};
+
+
+/**
+ * Provides a text input and draw interaction to allow a user to create and modify a ol.Feature (point geometry).
+ *
+ * The text input is provided by {@link ngeo.nominatimInputComponent} and includes Nominatim search.
+ *
+ * Example:
+ *
+ *
+ *
+ * Is used in in the partial of {@link ngeo.routingComponent}.
+ *
+ * See the [../examples/routing.html](../examples/routing.html) example for a usage sample.
+ *
+ * @htmlAttribute {ol.Map} ngeo-routing-feature-map The map.
+ * @htmlAttribute {ol.Feature} ngeo-routing-feature-feature The feature.
+ * @htmlAttribute {string} ngeo-routing-feature-fill-color The marker fill color.
+ * @htmlAttribute {string} ngeo-routing-feature-stroke-color The marker stroke color.
+ * @htmlAttribute {function(ol.Feature)} ngeo-routing-feature-on-change Event fired when feature changes.
+ * @ngdoc directive
+ * @ngname ngeoRoutingFeature
+ */
+ngeo.routing.RoutingFeatureComponent.component_ = {
+ controller: ngeo.routing.RoutingFeatureComponent.Controller,
+ bindings: {
+ 'map': '} coordinates coordinates of the route (at least two!)
+ * @param {?Object} config optional configuration
+ * @return {!angular.$http.HttpPromise} promise of the OSRM API request
+ */
+ngeo.routing.RoutingService.prototype.getRoute = function(coordinates, config) {
+
+ config = config || {};
+
+ // Service
+ // see: https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#requests
+ if (!config['service']) {
+ config['service'] = 'route'; // default to route
+ }
+
+ // Mode of transportation,
+ // can be: car, bike, foot
+ // see: https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#requests
+ //
+ // As of version 5.8.0, OSRM (server) does not support multiple profiles simultaneously.
+ // This means the value actually does not matter.
+ if (!config['profile']) {
+ config['profile'] = 'car'; // default to car
+ }
+
+ // build request URL
+ let url = this.ngeoOsrmBackendUrl_;
+
+ // Common workaround to provide multiple profiles (since it is not supported yet)
+ // Every profile runs on its own instance.
+ if (config['instance']) {
+ url += `${config['instance']}/`;
+ }
+
+ url += `${config['service']}/${this.protocolVersion_}/${config['profile']}/`;
+
+ // [ [a,b] , [c,d] ] -> 'a,b;c,d'
+ const coordinateString = coordinates.map(c => c.join(',')).join(';');
+
+ url += coordinateString;
+
+ // look for route service options
+ // see: https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#route-service
+ if (config.options) {
+ url += '?';
+ const options = [];
+ for (const option of Object.keys(config.options)) {
+ options.push(`${option}=${config.options[option]}`);
+ }
+ url += options.join('&');
+ }
+
+ return this.$http_.get(url);
+};
+
+/**
+ * Snaps a coordinate to the street network and returns the nearest match
+ * @param {ol.Coordinate} coordinate coordinate to query
+ * @param {?Object} config optional configuration
+ * @return {!angular.$http.HttpPromise} promise of the OSRM API request
+ * @see https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md#nearest-service
+ */
+ngeo.routing.RoutingService.prototype.getNearest = function(coordinate, config) {
+ config = config || {};
+
+ // service is always nearest
+ config['service'] = 'nearest';
+
+ // Mode of transportation
+ // If used in combination with a getRoute request, choose the same profile.
+ if (!config['profile']) {
+ config['profile'] = 'car'; // default to car
+ }
+
+ // build request URL
+ let url = this.ngeoOsrmBackendUrl_;
+
+ // Common workaround to provide multiple profiles (since it is not supported yet)
+ // Every profile runs on its own instance.
+ if (config['instance']) {
+ url += `${config['instance']}/`;
+ }
+
+ url += `${config['service']}/${this.protocolVersion_}/${config['profile']}/`;
+
+ // [a,b] -> 'a,b'
+ const coordinateString = coordinate.join(',');
+ url += coordinateString;
+
+ // look for nearest service options
+ if (config.options) {
+ url += '?';
+ const options = [];
+ for (const option of Object.keys(config.options)) {
+ options.push(`${option}=${config.options[option]}`);
+ }
+ url += options.join('&');
+ }
+
+ return this.$http_.get(url);
+};
+
+
+/**
+ * @type {!angular.Module}
+ */
+ngeo.routing.RoutingService.module = angular.module('ngeoRoutingService', [
+]);
+
+ngeo.routing.RoutingService.module.service('ngeoRoutingService', ngeo.routing.RoutingService);
diff --git a/src/routing/module.js b/src/routing/module.js
new file mode 100644
index 000000000000..1de54b30f246
--- /dev/null
+++ b/src/routing/module.js
@@ -0,0 +1,10 @@
+goog.provide('ngeo.routing.module');
+
+goog.require('ngeo.routing.RoutingComponent');
+
+/**
+ * @type {angular.Module}
+ */
+ngeo.routing.module = angular.module('ngeoRoutingModule', [
+ ngeo.routing.RoutingComponent.module.name
+]);
diff --git a/src/routing/nominatiminput.html b/src/routing/nominatiminput.html
new file mode 100644
index 000000000000..e1bc3491180f
--- /dev/null
+++ b/src/routing/nominatiminput.html
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/routing/routing.html b/src/routing/routing.html
new file mode 100644
index 000000000000..2fa13b199ad1
--- /dev/null
+++ b/src/routing/routing.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$ctrl.errorMessage}}
+
+
+
+
+
+
diff --git a/src/routing/routingfeature.html b/src/routing/routingfeature.html
new file mode 100644
index 000000000000..a39719106ebd
--- /dev/null
+++ b/src/routing/routingfeature.html
@@ -0,0 +1,12 @@
+
diff --git a/test/spec/services/routing.spec.js b/test/spec/services/routing.spec.js
new file mode 100644
index 000000000000..28d170e1ae4f
--- /dev/null
+++ b/test/spec/services/routing.spec.js
@@ -0,0 +1,75 @@
+goog.require('ngeo.RoutingService');
+
+describe('ngeo.RoutingService', () => {
+ let $httpBackend;
+
+ beforeEach(() => {
+ module('ngeo', ($provide) => {
+ $provide.value('ngeoRoutingOptions', {
+ 'backendUrl': 'http://example.com/'
+ });
+ });
+ });
+
+ afterEach(() => {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('Should get a route', () => {
+ let ngeoOsrmBackendUrl;
+ let ngeoRoutingService;
+ const successResponse = {
+ route: true
+ };
+
+ inject(($injector) => {
+ $httpBackend = $injector.get('$httpBackend');
+ ngeoRoutingService = $injector.get('ngeoRoutingService');
+ ngeoOsrmBackendUrl = $injector.get('ngeoRoutingOptions').backendUrl;
+ });
+
+ const coordinates = /** @type {Array.} */ [[6.455, 46.648], [6.532, 6.532]];
+ const coordString = `${coordinates[0][0]},${coordinates[0][1]};${coordinates[1][0]},${coordinates[1][1]}`;
+ const config = {
+ profile: 'jetpack',
+ options: {
+ option: 'value'
+ }
+ };
+ const requestUrl = `${ngeoOsrmBackendUrl}route/v1/${config.profile}/${coordString}?option=${config.options.option}`;
+
+ $httpBackend.when('GET', requestUrl).respond(successResponse);
+ $httpBackend.expectGET(requestUrl);
+ ngeoRoutingService.getRoute(coordinates, config);
+ $httpBackend.flush();
+ });
+
+ it('Should get a nearest object', () => {
+ let ngeoOsrmBackendUrl;
+ let ngeoRoutingService;
+ const successResponse = {
+ nearest: 'Streetname'
+ };
+
+ inject(($injector) => {
+ $httpBackend = $injector.get('$httpBackend');
+ ngeoRoutingService = $injector.get('ngeoRoutingService');
+ ngeoOsrmBackendUrl = $injector.get('ngeoRoutingOptions').backendUrl;
+ });
+
+ const coordinate = /** @type {ol.Coordinate} */ [6.455, 46.648];
+ const coordString = `${coordinate[0]},${coordinate[1]}`;
+ const config = {
+ options: {
+ option: 'value'
+ }
+ };
+ const requestUrl = `${ngeoOsrmBackendUrl}nearest/v1/car/${coordString}?option=${config.options.option}`;
+
+ $httpBackend.when('GET', requestUrl).respond(successResponse);
+ $httpBackend.expectGET(requestUrl);
+ ngeoRoutingService.getNearest(coordinate, config);
+ $httpBackend.flush();
+ });
+});