diff --git a/contribs/gmf/apps/desktop/Controller.js b/contribs/gmf/apps/desktop/Controller.js index 70e31cc0f0c2..cef2a8bf4b9b 100644 --- a/contribs/gmf/apps/desktop/Controller.js +++ b/contribs/gmf/apps/desktop/Controller.js @@ -16,6 +16,7 @@ import ngeoProjEPSG21781 from 'ngeo/proj/EPSG21781.js'; import * as olBase from 'ol/index.js'; import Raven from 'raven-js/src/raven.js'; import RavenPluginsAngular from 'raven-js/plugins/angular.js'; +import olSourceVector from 'ol/source/Vector.js'; if (!window.requestAnimationFrame) { alert('Your browser is not supported, please update it or use another one. You will be redirected.\n\n' @@ -123,6 +124,9 @@ exports.module = angular.module('Appdesktop', [ gmfControllersAbstractDesktopController.module.name, ]); +exports.module.value('ngeoSnappingTolerance', 20); +exports.module.value('ngeoSnappingSource', new olSourceVector()); + exports.module.value('gmfContextualdatacontentTemplateUrl', 'gmf/contextualdata'); exports.module.run(/* @ngInject */ ($templateCache) => { $templateCache.put('gmf/contextualdata', require('./contextualdata.html')); diff --git a/contribs/gmf/src/controllers/AbstractDesktopController.js b/contribs/gmf/src/controllers/AbstractDesktopController.js index 57a2c4e2ed5b..5e0e7ee3e423 100644 --- a/contribs/gmf/src/controllers/AbstractDesktopController.js +++ b/contribs/gmf/src/controllers/AbstractDesktopController.js @@ -254,5 +254,4 @@ exports.module.value('ngeoQueryOptions', { exports.module.value('ngeoMeasurePrecision', 3); exports.module.value('ngeoMeasureDecimals', 0); - export default exports; diff --git a/contribs/gmf/src/editing/Snapping.js b/contribs/gmf/src/editing/Snapping.js index 3aeccfcde82b..99e01d55a3c9 100644 --- a/contribs/gmf/src/editing/Snapping.js +++ b/contribs/gmf/src/editing/Snapping.js @@ -9,7 +9,9 @@ import * as olBase from 'ol/index.js'; import * as olEvents from 'ol/events.js'; import olCollection from 'ol/Collection.js'; import olFormatWFS from 'ol/format/WFS.js'; -import olInteractionSnap from 'ol/interaction/Snap.js'; +import olInteractionSnap, {handleEvent as snapHandleEvent} from 'ol/interaction/Snap.js'; +import {handleEvent as handlePointerEvent} from 'ol/interaction/Pointer.js'; + /** * The snapping service of GMF. Responsible of collecting the treeCtrls that @@ -26,6 +28,7 @@ import olInteractionSnap from 'ol/interaction/Snap.js'; * @param {angular.$http} $http Angular $http service. * @param {angular.$q} $q The Angular $q service. * @param {!angular.Scope} $rootScope Angular rootScope. + * @param {!angular.$injector} $injector Angular injector. * @param {angular.$timeout} $timeout Angular timeout service. * @param {gmf.theme.Themes} gmfThemes The gmf Themes service. * @param {gmf.layertree.TreeManager} gmfTreeManager The gmf TreeManager service. @@ -33,7 +36,7 @@ import olInteractionSnap from 'ol/interaction/Snap.js'; * @ngdoc service * @ngname gmfSnapping */ -const exports = function($http, $q, $rootScope, $timeout, gmfThemes, +const exports = function($http, $q, $rootScope, $injector, $timeout, gmfThemes, gmfTreeManager) { // === Injected services === @@ -62,6 +65,12 @@ const exports = function($http, $q, $rootScope, $timeout, gmfThemes, */ this.timeout_ = $timeout; + /** + * @type {!angular.$injector} + * @private + */ + this.injector_ = $injector; + /** * @type {gmf.theme.Themes} * @private @@ -113,13 +122,39 @@ const exports = function($http, $q, $rootScope, $timeout, gmfThemes, */ this.ogcServers_ = null; + /** + * @type {ol.source.Vector | undefined} + * @private + */ + this.ngeoSnappingSource_ = this.injector_.get('ngeoSnappingSource') || undefined; + }; +class CustomSnap extends olInteractionSnap { + constructor(options) { + super(options); + this.modifierPressed = false; + document.body.addEventListener('keydown', (evt) => { + this.modifierPressed = evt.keyCode === 17; // Ctrl key + }); + document.body.addEventListener('keyup', () => { + this.modifierPressed = false; + }); + this.handleEvent = (evt) => { // horrible hack + if (!this.modifierPressed) { + return snapHandleEvent.call(this, evt); + } + return handlePointerEvent.call(this, evt); + }; + } +} + + /** * In order for a `ol.interaction.Snap` to work properly, it has to be added * to the map after any draw interactions or other kinds of interactions that - * ineracts with features on the map. + * interacts with features on the map. * * This method can be called to make sure the Snap interactions are on top. * @@ -131,7 +166,7 @@ exports.prototype.ensureSnapInteractionsOnTop = function() { let item; for (const uid in this.cache_) { - item = this.cache_[+uid]; + item = this.cache_[uid]; if (item.active) { googAsserts.assert(item.interaction); map.removeInteraction(item.interaction); @@ -258,11 +293,11 @@ exports.prototype.registerTreeCtrl_ = function(treeCtrl) { */ exports.prototype.unregisterAllTreeCtrl_ = function() { for (const uid in this.cache_) { - const item = this.cache_[+uid]; + const item = this.cache_[uid]; if (item) { item.stateWatcherUnregister(); this.deactivateItem_(item); - delete this.cache_[+uid]; + delete this.cache_[uid]; } } }; @@ -384,7 +419,7 @@ exports.prototype.activateItem_ = function(item) { const map = this.map_; googAsserts.assert(map); - const interaction = new olInteractionSnap({ + const interaction = new CustomSnap({ edge: item.snappingConfig.edge, features: item.features, pixelTolerance: item.snappingConfig.tolerance, @@ -431,6 +466,7 @@ exports.prototype.deactivateItem_ = function(item) { } item.active = false; + this.refreshSnappingSource_(); }; @@ -441,7 +477,7 @@ exports.prototype.loadAllItems_ = function() { this.mapViewChangePromise_ = null; let item; for (const uid in this.cache_) { - item = this.cache_[+uid]; + item = this.cache_[uid]; if (item.active) { this.loadItemFeatures_(item); } @@ -514,6 +550,7 @@ exports.prototype.loadItemFeatures_ = function(item) { const readFeatures = new olFormatWFS().readFeatures(response.data); if (readFeatures) { item.features.extend(readFeatures); + this.refreshSnappingSource_(); } }); @@ -535,6 +572,18 @@ exports.prototype.handleMapMoveEnd_ = function() { ); }; +/** + * @private + */ +exports.prototype.refreshSnappingSource_ = function() { + this.ngeoSnappingSource_.clear(); + for (const uid in this.cache_) { + const item = this.cache_[uid]; + if (item.active) { + this.ngeoSnappingSource_.addFeatures(item.features.getArray()); + } + } +}; /** * @typedef {Object} diff --git a/examples/drawfeature.js b/examples/drawfeature.js index ef86becf03c8..901a1458d1ee 100644 --- a/examples/drawfeature.js +++ b/examples/drawfeature.js @@ -25,7 +25,6 @@ exports.module = angular.module('app', [ ngeoMiscToolActivateMgr.module.name, ]); - /** * @param {!angular.Scope} $scope Angular scope. * @param {ol.Collection.} ngeoFeatures Collection of features. diff --git a/src/editing/createfeatureComponent.js b/src/editing/createfeatureComponent.js index 9d2b484b4143..9d5cc5a2b671 100644 --- a/src/editing/createfeatureComponent.js +++ b/src/editing/createfeatureComponent.js @@ -79,6 +79,7 @@ exports.directive('ngeoCreatefeature', exports.directive_); * @param {!angularGettext.Catalog} gettextCatalog Gettext catalog. * @param {!angular.$compile} $compile Angular compile service. * @param {!angular.$filter} $filter Angular filter + * @param {!angular.$injector} $injector Angular injector service. * @param {!angular.Scope} $scope Scope. * @param {!angular.$timeout} $timeout Angular timeout service. * @param {!ngeo.misc.EventHelper} ngeoEventHelper Ngeo event helper service @@ -89,7 +90,7 @@ exports.directive('ngeoCreatefeature', exports.directive_); * @ngdoc controller * @ngname ngeoCreatefeatureController */ -exports.Controller_ = function(gettextCatalog, $compile, $filter, $scope, +exports.Controller_ = function(gettextCatalog, $compile, $filter, $injector, $scope, $timeout, ngeoEventHelper) { /** @@ -152,6 +153,12 @@ exports.Controller_ = function(gettextCatalog, $compile, $filter, $scope, */ this.ngeoEventHelper_ = ngeoEventHelper; + /** + * @type {!angular.$injector} + * @private + */ + this.injector_ = $injector; + /** * The draw or measure interaction responsible of drawing the vector feature. * The actual type depends on the geometry type. @@ -201,7 +208,9 @@ exports.Controller_.prototype.$onInit = function() { { style: new olStyleStyle(), startMsg: this.compile_(`
${helpMsg}
`)(this.scope_)[0], - continueMsg: this.compile_(`
${contMsg}
`)(this.scope_)[0] + continueMsg: this.compile_(`
${contMsg}
`)(this.scope_)[0], + tolerance: this.injector_.has('ngeoSnappingTolerance') ? this.injector_.get('ngeoSnappingTolerance') : undefined, + source: this.injector_.has('ngeoSnappingSource') ? this.injector_.get('ngeoSnappingSource') : undefined, } ); } else if (this.geomType === ngeoGeometryType.POLYGON || diff --git a/src/interaction/MeasureLength.js b/src/interaction/MeasureLength.js index a1763e12f60f..2609501c94fa 100644 --- a/src/interaction/MeasureLength.js +++ b/src/interaction/MeasureLength.js @@ -6,6 +6,11 @@ import ngeoInteractionMeasure from 'ngeo/interaction/Measure.js'; import * as olBase from 'ol/index.js'; import olGeomLineString from 'ol/geom/LineString.js'; import olInteractionDraw from 'ol/interaction/Draw.js'; +import {distance} from 'ol/coordinate.js'; +import {containsXY} from 'ol/extent'; + +let modifierPressed = undefined; + /** * @classdesc @@ -24,6 +29,15 @@ const exports = function(format, gettextCatalog, options = /** @type {ngeox.inte ngeoInteractionMeasure.call(this, /** @type {ngeo.interaction.MeasureBaseOptions} */ (options)); + if (modifierPressed === undefined) { + modifierPressed = false; + document.body.addEventListener('keydown', (evt) => { + modifierPressed = evt.keyCode === 17; // Ctrl key + }); + document.body.addEventListener('keyup', () => { + modifierPressed = false; + }); + } if (options.continueMsg !== undefined) { this.continueMsg = options.continueMsg; @@ -41,6 +55,17 @@ const exports = function(format, gettextCatalog, options = /** @type {ngeox.inte */ this.format = format; + /** + * The snapping tolerance in pixels. + * @params {number} + */ + this.tolerance = options.tolerance; + + /** + * The snapping source + * @params {ol.source.Vector} + */ + this.source = options.source; }; olBase.inherits(exports, ngeoInteractionMeasure); @@ -52,12 +77,86 @@ olBase.inherits(exports, ngeoInteractionMeasure); exports.prototype.createDrawInteraction = function(style, source) { return new olInteractionDraw({ type: /** @type {ol.geom.GeometryType} */ ('LineString'), + geometryFunction: this.linestringGeometryFunction.bind(this), + condition: () => true, + style: style, source: source, - style: style }); }; +/** + * Create a `linestringGeometryFunction` that will create a line string with segments + * snapped to π/4 angle. + * Use this with the draw interaction and `type: 'LineString'`. + * @param {LineCoordType} coordinates Coordinates. + * @param {LineString=} opt_geometry Geometry. + * @return {LineString} Geometry. + */ +exports.prototype.linestringGeometryFunction = function(coordinates, opt_geometry) { + if (modifierPressed) { + const viewRotation = this.getMap().getView().getRotation(); + const angle = Math.PI / 4; + const from = coordinates[coordinates.length - 2]; + const to = coordinates[coordinates.length - 1]; + const dx = from[0] - to[0]; + const dy = from[1] - to[1]; + const length = distance(from, to); + const rotation = viewRotation + Math.round((Math.atan2(dy, dx) - viewRotation) / angle) * angle; + + to[0] = from[0] - (length * Math.cos(rotation)); + to[1] = from[1] - (length * Math.sin(rotation)); + + if (this.tolerance !== undefined && this.source !== undefined) { + const delta = this.getMap().getView().getResolution() * this.tolerance; + const bbox = [to[0] - delta, to[1] - delta, to[0] + delta, to[1] + delta]; + + const layerSource = this.source; + const featuresInExtent = layerSource.getFeaturesInExtent(bbox); + featuresInExtent.forEach((feature) => { + + let lastIntersection = []; + let bestIntersection = []; + let bestDistance = Infinity; + + // Line points are: from A to M (to B that we need to find) + const distanceFromTo = distance(from, to); + const ax = from[0]; + const ay = from[1]; + const mx = to[0]; + const my = to[1]; + const unitVector = [(mx - ax) / distanceFromTo, (my - ay) / distanceFromTo]; + const b = [(ax + (distanceFromTo + delta) * unitVector[0]), (ay + (distanceFromTo + delta) * unitVector[1])]; + + feature.getGeometry().forEachSegment((point1, point2) => { + // intersection calculation + lastIntersection = this.computeLineSegmentIntersection(from, b, point1, point2); + if (lastIntersection !== undefined && containsXY(bbox, lastIntersection[0], lastIntersection[1])) { + const lastDistance = distance(to, lastIntersection); + if (lastDistance < bestDistance) { + bestDistance = lastDistance; + bestIntersection = lastIntersection; + } + } + }); + + if (bestIntersection) { + to[0] = bestIntersection[0] || to[0]; + to[1] = bestIntersection[1] || to[1]; + } + }); + } + } + + const geometry = opt_geometry; + if (geometry) { + geometry.setCoordinates(coordinates); + return geometry; + } + return new olGeomLineString(coordinates); +}; + + /** * @inheritDoc */ @@ -70,5 +169,38 @@ exports.prototype.handleMeasure = function(callback) { callback(output, coord); }; +/** + * Compute the intersection between 2 segments + * + * @param {Number} line1vertex1 The coordinates of the first line's first vertex. + * @param {Number} line1vertex2 The coordinates of the first line's second vertex. + * @param {Number} line2vertex1 The coordinates of the second line's first vertex. + * @param {Number} line2vertex2 The coordinates of the second line's second vertex. + * @return {Array | undefined} The intersection point, undefined if there is no intersection point or lines are coincident. + */ +exports.prototype.computeLineSegmentIntersection = function(line1vertex1, line1vertex2, line2vertex1, line2vertex2) { + const numerator1A = (line2vertex2[0] - line2vertex1[0]) * (line1vertex1[1] - line2vertex1[1]) + - (line2vertex2[1] - line2vertex1[1]) * (line1vertex1[0] - line2vertex1[0]); + const numerator1B = (line1vertex2[0] - line1vertex1[0]) * (line1vertex1[1] - line2vertex1[1]) + - (line1vertex2[1] - line1vertex1[1]) * (line1vertex1[0] - line2vertex1[0]); + const denominator1 = (line2vertex2[1] - line2vertex1[1]) * (line1vertex2[0] - line1vertex1[0]) + - (line2vertex2[0] - line2vertex1[0]) * (line1vertex2[1] - line1vertex1[1]); + + // If denominator = 0, then lines are parallel. If denominator = 0 and both numerators are 0, then coincident + if (denominator1 === 0) { + return; + } + + const ua1 = numerator1A / denominator1; + const ub1 = numerator1B / denominator1; + + if (ua1 >= 0 && ua1 <= 1 && ub1 >= 0 && ub1 <= 1) { + const result = []; + result[0] = line1vertex1[0] + ua1 * (line1vertex2[0] - line1vertex1[0]); + result[1] = line1vertex1[1] + ua1 * (line1vertex2[1] - line1vertex1[1]); + return result; + } +}; + export default exports; diff --git a/src/measure/length.js b/src/measure/length.js index fff95c946c80..22f8d2c0302a 100644 --- a/src/measure/length.js +++ b/src/measure/length.js @@ -47,7 +47,9 @@ exports.directive_ = function($compile, gettextCatalog, $filter, $injector) { style: new olStyleStyle(), startMsg: $compile(`
${helpMsg}
`)($scope)[0], continueMsg: $compile(`
${contMsg}
`)($scope)[0], - precision: $injector.has('ngeoMeasurePrecision') ? $injector.get('ngeoMeasurePrecision') : undefined + precision: $injector.has('ngeoMeasurePrecision') ? $injector.get('ngeoMeasurePrecision') : undefined, + tolerance: $injector.has('ngeoSnappingTolerance') ? $injector.get('ngeoSnappingTolerance') : undefined, + source: $injector.has('ngeoSnappingSource') ? $injector.get('ngeoSnappingSource') : undefined, }); drawFeatureCtrl.registerInteraction(measureLength);