Snapping.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. /* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for
  2. * full list of contributors). Published under the 2-clause BSD license.
  3. * See license.txt in the OpenLayers distribution or repository for the
  4. * full text of the license. */
  5. /**
  6. * @requires OpenLayers/Control.js
  7. * @requires OpenLayers/Layer/Vector.js
  8. */
  9. /**
  10. * Class: OpenLayers.Control.Snapping
  11. * Acts as a snapping agent while editing vector features.
  12. *
  13. * Inherits from:
  14. * - <OpenLayers.Control>
  15. */
  16. OpenLayers.Control.Snapping = OpenLayers.Class(OpenLayers.Control, {
  17. /**
  18. * APIProperty: events
  19. * {<OpenLayers.Events>} Events instance for listeners and triggering
  20. * control specific events.
  21. *
  22. * Register a listener for a particular event with the following syntax:
  23. * (code)
  24. * control.events.register(type, obj, listener);
  25. * (end)
  26. *
  27. * Supported event types (in addition to those from <OpenLayers.Control.events>):
  28. * beforesnap - Triggered before a snap occurs. Listeners receive an
  29. * event object with *point*, *x*, *y*, *distance*, *layer*, and
  30. * *snapType* properties. The point property will be original point
  31. * geometry considered for snapping. The x and y properties represent
  32. * coordinates the point will receive. The distance is the distance
  33. * of the snap. The layer is the target layer. The snapType property
  34. * will be one of "node", "vertex", or "edge". Return false to stop
  35. * snapping from occurring.
  36. * snap - Triggered when a snap occurs. Listeners receive an event with
  37. * *point*, *snapType*, *layer*, and *distance* properties. The point
  38. * will be the location snapped to. The snapType will be one of "node",
  39. * "vertex", or "edge". The layer will be the target layer. The
  40. * distance will be the distance of the snap in map units.
  41. * unsnap - Triggered when a vertex is unsnapped. Listeners receive an
  42. * event with a *point* property.
  43. */
  44. /**
  45. * CONSTANT: DEFAULTS
  46. * Default target properties.
  47. */
  48. DEFAULTS: {
  49. tolerance: 10,
  50. node: true,
  51. edge: true,
  52. vertex: true
  53. },
  54. /**
  55. * Property: greedy
  56. * {Boolean} Snap to closest feature in first layer with an eligible
  57. * feature. Default is true.
  58. */
  59. greedy: true,
  60. /**
  61. * Property: precedence
  62. * {Array} List representing precedence of different snapping types.
  63. * Default is "node", "vertex", "edge".
  64. */
  65. precedence: ["node", "vertex", "edge"],
  66. /**
  67. * Property: resolution
  68. * {Float} The map resolution for the previously considered snap.
  69. */
  70. resolution: null,
  71. /**
  72. * Property: geoToleranceCache
  73. * {Object} A cache of geo-tolerances. Tolerance values (in map units) are
  74. * calculated when the map resolution changes.
  75. */
  76. geoToleranceCache: null,
  77. /**
  78. * Property: layer
  79. * {<OpenLayers.Layer.Vector>} The current editable layer. Set at
  80. * construction or after construction with <setLayer>.
  81. */
  82. layer: null,
  83. /**
  84. * Property: feature
  85. * {<OpenLayers.Feature.Vector>} The current editable feature.
  86. */
  87. feature: null,
  88. /**
  89. * Property: point
  90. * {<OpenLayers.Geometry.Point>} The currently snapped vertex.
  91. */
  92. point: null,
  93. /**
  94. * Constructor: OpenLayers.Control.Snapping
  95. * Creates a new snapping control. A control is constructed with an editable
  96. * layer and a set of configuration objects for target layers. While the
  97. * control is active, dragging vertices while drawing new features or
  98. * modifying existing features on the editable layer will engage
  99. * snapping to features on the target layers. Whether a vertex snaps to
  100. * a feature on a target layer depends on the target layer configuration.
  101. *
  102. * Parameters:
  103. * options - {Object} An object containing all configuration properties for
  104. * the control.
  105. *
  106. * Valid options:
  107. * layer - {<OpenLayers.Layer.Vector>} The editable layer. Features from this
  108. * layer that are digitized or modified may have vertices snapped to
  109. * features from any of the target layers.
  110. * targets - {Array(Object | OpenLayers.Layer.Vector)} A list of objects for
  111. * configuring target layers. See valid properties of the target
  112. * objects below. If the items in the targets list are vector layers
  113. * (instead of configuration objects), the defaults from the <defaults>
  114. * property will apply. The editable layer itself may be a target
  115. * layer, allowing newly created or edited features to be snapped to
  116. * existing features from the same layer. If no targets are provided
  117. * the layer given in the constructor (as <layer>) will become the
  118. * initial target.
  119. * defaults - {Object} An object with default properties to be applied
  120. * to all target objects.
  121. * greedy - {Boolean} Snap to closest feature in first target layer that
  122. * applies. Default is true. If false, all features in all target
  123. * layers will be checked and the closest feature in all target layers
  124. * will be chosen. The greedy property determines if the order of the
  125. * target layers is significant. By default, the order of the target
  126. * layers is significant where layers earlier in the target layer list
  127. * have precedence over layers later in the list. Within a single
  128. * layer, the closest feature is always chosen for snapping. This
  129. * property only determines whether the search for a closer feature
  130. * continues after an eligible feature is found in a target layer.
  131. *
  132. * Valid target properties:
  133. * layer - {<OpenLayers.Layer.Vector>} A target layer. Features from this
  134. * layer will be eligible to act as snapping target for the editable
  135. * layer.
  136. * tolerance - {Float} The distance (in pixels) at which snapping may occur.
  137. * Default is 10.
  138. * node - {Boolean} Snap to nodes (first or last point in a geometry) in
  139. * target layer. Default is true.
  140. * nodeTolerance - {Float} Optional distance at which snapping may occur
  141. * for nodes specifically. If none is provided, <tolerance> will be
  142. * used.
  143. * vertex - {Boolean} Snap to vertices in target layer. Default is true.
  144. * vertexTolerance - {Float} Optional distance at which snapping may occur
  145. * for vertices specifically. If none is provided, <tolerance> will be
  146. * used.
  147. * edge - {Boolean} Snap to edges in target layer. Default is true.
  148. * edgeTolerance - {Float} Optional distance at which snapping may occur
  149. * for edges specifically. If none is provided, <tolerance> will be
  150. * used.
  151. * filter - {<OpenLayers.Filter>} Optional filter to evaluate to determine if
  152. * feature is eligible for snapping. If filter evaluates to true for a
  153. * target feature a vertex may be snapped to the feature.
  154. * minResolution - {Number} If a minResolution is provided, snapping to this
  155. * target will only be considered if the map resolution is greater than
  156. * or equal to this value (the minResolution is inclusive). Default is
  157. * no minimum resolution limit.
  158. * maxResolution - {Number} If a maxResolution is provided, snapping to this
  159. * target will only be considered if the map resolution is strictly
  160. * less than this value (the maxResolution is exclusive). Default is
  161. * no maximum resolution limit.
  162. */
  163. initialize: function(options) {
  164. OpenLayers.Control.prototype.initialize.apply(this, [options]);
  165. this.options = options || {}; // TODO: this could be done by the super
  166. // set the editable layer if provided
  167. if(this.options.layer) {
  168. this.setLayer(this.options.layer);
  169. }
  170. // configure target layers
  171. var defaults = OpenLayers.Util.extend({}, this.options.defaults);
  172. this.defaults = OpenLayers.Util.applyDefaults(defaults, this.DEFAULTS);
  173. this.setTargets(this.options.targets);
  174. if(this.targets.length === 0 && this.layer) {
  175. this.addTargetLayer(this.layer);
  176. }
  177. this.geoToleranceCache = {};
  178. },
  179. /**
  180. * APIMethod: setLayer
  181. * Set the editable layer. Call the setLayer method if the editable layer
  182. * changes and the same control should be used on a new editable layer.
  183. * If the control is already active, it will be active after the new
  184. * layer is set.
  185. *
  186. * Parameters:
  187. * layer - {<OpenLayers.Layer.Vector>} The new editable layer.
  188. */
  189. setLayer: function(layer) {
  190. if(this.active) {
  191. this.deactivate();
  192. this.layer = layer;
  193. this.activate();
  194. } else {
  195. this.layer = layer;
  196. }
  197. },
  198. /**
  199. * Method: setTargets
  200. * Set the targets for the snapping agent.
  201. *
  202. * Parameters:
  203. * targets - {Array} An array of target configs or target layers.
  204. */
  205. setTargets: function(targets) {
  206. this.targets = [];
  207. if(targets && targets.length) {
  208. var target;
  209. for(var i=0, len=targets.length; i<len; ++i) {
  210. target = targets[i];
  211. if(target instanceof OpenLayers.Layer.Vector) {
  212. this.addTargetLayer(target);
  213. } else {
  214. this.addTarget(target);
  215. }
  216. }
  217. }
  218. },
  219. /**
  220. * Method: addTargetLayer
  221. * Add a target layer with the default target config.
  222. *
  223. * Parameters:
  224. * layer - {<OpenLayers.Layer.Vector>} A target layer.
  225. */
  226. addTargetLayer: function(layer) {
  227. this.addTarget({layer: layer});
  228. },
  229. /**
  230. * Method: addTarget
  231. * Add a configured target layer.
  232. *
  233. * Parameters:
  234. * target - {Object} A target config.
  235. */
  236. addTarget: function(target) {
  237. target = OpenLayers.Util.applyDefaults(target, this.defaults);
  238. target.nodeTolerance = target.nodeTolerance || target.tolerance;
  239. target.vertexTolerance = target.vertexTolerance || target.tolerance;
  240. target.edgeTolerance = target.edgeTolerance || target.tolerance;
  241. this.targets.push(target);
  242. },
  243. /**
  244. * Method: removeTargetLayer
  245. * Remove a target layer.
  246. *
  247. * Parameters:
  248. * layer - {<OpenLayers.Layer.Vector>} The target layer to remove.
  249. */
  250. removeTargetLayer: function(layer) {
  251. var target;
  252. for(var i=this.targets.length-1; i>=0; --i) {
  253. target = this.targets[i];
  254. if(target.layer === layer) {
  255. this.removeTarget(target);
  256. }
  257. }
  258. },
  259. /**
  260. * Method: removeTarget
  261. * Remove a target.
  262. *
  263. * Parameters:
  264. * target - {Object} A target config.
  265. *
  266. * Returns:
  267. * {Array} The targets array.
  268. */
  269. removeTarget: function(target) {
  270. return OpenLayers.Util.removeItem(this.targets, target);
  271. },
  272. /**
  273. * APIMethod: activate
  274. * Activate the control. Activating the control registers listeners for
  275. * editing related events so that during feature creation and
  276. * modification, moving vertices will trigger snapping.
  277. */
  278. activate: function() {
  279. var activated = OpenLayers.Control.prototype.activate.call(this);
  280. if(activated) {
  281. if(this.layer && this.layer.events) {
  282. this.layer.events.on({
  283. sketchstarted: this.onSketchModified,
  284. sketchmodified: this.onSketchModified,
  285. vertexmodified: this.onVertexModified,
  286. scope: this
  287. });
  288. }
  289. }
  290. return activated;
  291. },
  292. /**
  293. * APIMethod: deactivate
  294. * Deactivate the control. Deactivating the control unregisters listeners
  295. * so feature editing may proceed without engaging the snapping agent.
  296. */
  297. deactivate: function() {
  298. var deactivated = OpenLayers.Control.prototype.deactivate.call(this);
  299. if(deactivated) {
  300. if(this.layer && this.layer.events) {
  301. this.layer.events.un({
  302. sketchstarted: this.onSketchModified,
  303. sketchmodified: this.onSketchModified,
  304. vertexmodified: this.onVertexModified,
  305. scope: this
  306. });
  307. }
  308. }
  309. this.feature = null;
  310. this.point = null;
  311. return deactivated;
  312. },
  313. /**
  314. * Method: onSketchModified
  315. * Registered as a listener for the sketchmodified event on the editable
  316. * layer.
  317. *
  318. * Parameters:
  319. * event - {Object} The sketch modified event.
  320. */
  321. onSketchModified: function(event) {
  322. this.feature = event.feature;
  323. this.considerSnapping(event.vertex, event.vertex);
  324. },
  325. /**
  326. * Method: onVertexModified
  327. * Registered as a listener for the vertexmodified event on the editable
  328. * layer.
  329. *
  330. * Parameters:
  331. * event - {Object} The vertex modified event.
  332. */
  333. onVertexModified: function(event) {
  334. this.feature = event.feature;
  335. var loc = this.layer.map.getLonLatFromViewPortPx(event.pixel);
  336. this.considerSnapping(
  337. event.vertex, new OpenLayers.Geometry.Point(loc.lon, loc.lat)
  338. );
  339. },
  340. /**
  341. * Method: considerSnapping
  342. *
  343. * Parameters:
  344. * point - {<OpenLayers.Geometry.Point>} The vertex to be snapped (or
  345. * unsnapped).
  346. * loc - {<OpenLayers.Geometry.Point>} The location of the mouse in map
  347. * coords.
  348. */
  349. considerSnapping: function(point, loc) {
  350. var best = {
  351. rank: Number.POSITIVE_INFINITY,
  352. dist: Number.POSITIVE_INFINITY,
  353. x: null, y: null
  354. };
  355. var snapped = false;
  356. var result, target;
  357. for(var i=0, len=this.targets.length; i<len; ++i) {
  358. target = this.targets[i];
  359. result = this.testTarget(target, loc);
  360. if(result) {
  361. if(this.greedy) {
  362. best = result;
  363. best.target = target;
  364. snapped = true;
  365. break;
  366. } else {
  367. if((result.rank < best.rank) ||
  368. (result.rank === best.rank && result.dist < best.dist)) {
  369. best = result;
  370. best.target = target;
  371. snapped = true;
  372. }
  373. }
  374. }
  375. }
  376. if(snapped) {
  377. var proceed = this.events.triggerEvent("beforesnap", {
  378. point: point, x: best.x, y: best.y, distance: best.dist,
  379. layer: best.target.layer, snapType: this.precedence[best.rank]
  380. });
  381. if(proceed !== false) {
  382. point.x = best.x;
  383. point.y = best.y;
  384. this.point = point;
  385. this.events.triggerEvent("snap", {
  386. point: point,
  387. snapType: this.precedence[best.rank],
  388. layer: best.target.layer,
  389. distance: best.dist
  390. });
  391. } else {
  392. snapped = false;
  393. }
  394. }
  395. if(this.point && !snapped) {
  396. point.x = loc.x;
  397. point.y = loc.y;
  398. this.point = null;
  399. this.events.triggerEvent("unsnap", {point: point});
  400. }
  401. },
  402. /**
  403. * Method: testTarget
  404. *
  405. * Parameters:
  406. * target - {Object} Object with target layer configuration.
  407. * loc - {<OpenLayers.Geometry.Point>} The location of the mouse in map
  408. * coords.
  409. *
  410. * Returns:
  411. * {Object} A result object with rank, dist, x, and y properties.
  412. * Returns null if candidate is not eligible for snapping.
  413. */
  414. testTarget: function(target, loc) {
  415. var resolution = this.layer.map.getResolution();
  416. if ("minResolution" in target) {
  417. if (resolution < target.minResolution) {
  418. return null;
  419. }
  420. }
  421. if ("maxResolution" in target) {
  422. if (resolution >= target.maxResolution) {
  423. return null;
  424. }
  425. }
  426. var tolerance = {
  427. node: this.getGeoTolerance(target.nodeTolerance, resolution),
  428. vertex: this.getGeoTolerance(target.vertexTolerance, resolution),
  429. edge: this.getGeoTolerance(target.edgeTolerance, resolution)
  430. };
  431. // this could be cached if we don't support setting tolerance values directly
  432. var maxTolerance = Math.max(
  433. tolerance.node, tolerance.vertex, tolerance.edge
  434. );
  435. var result = {
  436. rank: Number.POSITIVE_INFINITY, dist: Number.POSITIVE_INFINITY
  437. };
  438. var eligible = false;
  439. var features = target.layer.features;
  440. var feature, type, vertices, vertex, closest, dist, found;
  441. var numTypes = this.precedence.length;
  442. var ll = new OpenLayers.LonLat(loc.x, loc.y);
  443. for(var i=0, len=features.length; i<len; ++i) {
  444. feature = features[i];
  445. if(feature !== this.feature && !feature._sketch &&
  446. feature.state !== OpenLayers.State.DELETE &&
  447. (!target.filter || target.filter.evaluate(feature))) {
  448. if(feature.atPoint(ll, maxTolerance, maxTolerance)) {
  449. for(var j=0, stop=Math.min(result.rank+1, numTypes); j<stop; ++j) {
  450. type = this.precedence[j];
  451. if(target[type]) {
  452. if(type === "edge") {
  453. closest = feature.geometry.distanceTo(loc, {details: true});
  454. dist = closest.distance;
  455. if(dist <= tolerance[type] && dist < result.dist) {
  456. result = {
  457. rank: j, dist: dist,
  458. x: closest.x0, y: closest.y0 // closest coords on feature
  459. };
  460. eligible = true;
  461. // don't look for lower precedence types for this feature
  462. break;
  463. }
  464. } else {
  465. // look for nodes or vertices
  466. vertices = feature.geometry.getVertices(type === "node");
  467. found = false;
  468. for(var k=0, klen=vertices.length; k<klen; ++k) {
  469. vertex = vertices[k];
  470. dist = vertex.distanceTo(loc);
  471. if(dist <= tolerance[type] &&
  472. (j < result.rank || (j === result.rank && dist < result.dist))) {
  473. result = {
  474. rank: j, dist: dist,
  475. x: vertex.x, y: vertex.y
  476. };
  477. eligible = true;
  478. found = true;
  479. }
  480. }
  481. if(found) {
  482. // don't look for lower precedence types for this feature
  483. break;
  484. }
  485. }
  486. }
  487. }
  488. }
  489. }
  490. }
  491. return eligible ? result : null;
  492. },
  493. /**
  494. * Method: getGeoTolerance
  495. * Calculate a tolerance in map units given a tolerance in pixels. This
  496. * takes advantage of the <geoToleranceCache> when the map resolution
  497. * has not changed.
  498. *
  499. * Parameters:
  500. * tolerance - {Number} A tolerance value in pixels.
  501. * resolution - {Number} Map resolution.
  502. *
  503. * Returns:
  504. * {Number} A tolerance value in map units.
  505. */
  506. getGeoTolerance: function(tolerance, resolution) {
  507. if(resolution !== this.resolution) {
  508. this.resolution = resolution;
  509. this.geoToleranceCache = {};
  510. }
  511. var geoTolerance = this.geoToleranceCache[tolerance];
  512. if(geoTolerance === undefined) {
  513. geoTolerance = tolerance * resolution;
  514. this.geoToleranceCache[tolerance] = geoTolerance;
  515. }
  516. return geoTolerance;
  517. },
  518. /**
  519. * Method: destroy
  520. * Clean up the control.
  521. */
  522. destroy: function() {
  523. if(this.active) {
  524. this.deactivate(); // TODO: this should be handled by the super
  525. }
  526. delete this.layer;
  527. delete this.targets;
  528. OpenLayers.Control.prototype.destroy.call(this);
  529. },
  530. CLASS_NAME: "OpenLayers.Control.Snapping"
  531. });