From 6f9d05fc40cf6b3873485817a5994ed7f9c1bb0b Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Fri, 13 Dec 2013 08:49:45 -0500 Subject: [PATCH] implement Canvas events and SVG click-through --- build/deps.js | 4 +- debug/vector/vector2.html | 2 +- .../vector/canvas/CircleMarker.Canvas.js | 9 - src/layer/vector/canvas/Path.Canvas.js | 192 ------------------ src/layer/vector/canvas/Polygon.Canvas.js | 37 ---- src/layer/vector/canvas/Polyline.Canvas.js | 30 --- src/layer/vector2/Canvas.js | 98 ++++++++- src/layer/vector2/Path.js | 12 +- src/layer/vector2/SVG.js | 7 +- 9 files changed, 100 insertions(+), 291 deletions(-) delete mode 100644 src/layer/vector/canvas/CircleMarker.Canvas.js delete mode 100644 src/layer/vector/canvas/Path.Canvas.js delete mode 100644 src/layer/vector/canvas/Polygon.Canvas.js delete mode 100644 src/layer/vector/canvas/Polyline.Canvas.js diff --git a/build/deps.js b/build/deps.js index 4bd7e414..f77fe7ac 100644 --- a/build/deps.js +++ b/build/deps.js @@ -155,14 +155,14 @@ var deps = { 'layer/vector2/Renderer.js', 'layer/vector2/SVG.js', 'layer/vector2/SVG.VML.js', - 'layer/vector2/Canvas.js', 'layer/vector2/Path.js', 'layer/vector2/Path.Popup.js', 'geometry/LineUtil.js', 'layer/vector2/Polyline.js', 'geometry/PolyUtil.js', 'layer/vector2/Polygon.js', - 'layer/vector2/Rectangle.js' + 'layer/vector2/Rectangle.js', + 'layer/vector2/Canvas.js' ], desc: 'New vector layers implementation.' }, diff --git a/debug/vector/vector2.html b/debug/vector/vector2.html index 5ef3b0cc..f455573f 100644 --- a/debug/vector/vector2.html +++ b/debug/vector/vector2.html @@ -26,7 +26,7 @@ map.addLayer(L.marker(latlngs[0])); map.addLayer(L.marker(latlngs[len - 1])); - var path = L.polygon([[latlngs, [[50.5, 30.5], [50.5, 40], [40, 40]]], [[20, 0], [20, 40], [0, 40]]]).addTo(map); + var path = L.polygon([[latlngs, [[50.5, 30.5], [50.5, 40], [40, 40]]], [[20, 0], [20, 40], [0, 40]]], {renderer: L.canvas()}).addTo(map); var poly = L.polyline([[[60, 30], [60, 50], [40, 50]], [[20, 50], [20, 70], [0, 70]]], {color: 'red'}).addTo(map); map.fitBounds(path); diff --git a/src/layer/vector/canvas/CircleMarker.Canvas.js b/src/layer/vector/canvas/CircleMarker.Canvas.js deleted file mode 100644 index 627f7ed5..00000000 --- a/src/layer/vector/canvas/CircleMarker.Canvas.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * CircleMarker canvas specific drawing parts. - */ - -L.CircleMarker.include(!L.Path.CANVAS ? {} : { - _updateStyle: function () { - L.Path.prototype._updateStyle.call(this); - } -}); diff --git a/src/layer/vector/canvas/Path.Canvas.js b/src/layer/vector/canvas/Path.Canvas.js deleted file mode 100644 index 18c7d883..00000000 --- a/src/layer/vector/canvas/Path.Canvas.js +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Vector rendering for all browsers that support canvas. - */ - -L.Browser.canvas = (function () { - return !!document.createElement('canvas').getContext; -}()); - -L.Path = (L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? L.Path : L.Path.extend({ - statics: { - //CLIP_PADDING: 0.02, // not sure if there's a need to set it to a small value - CANVAS: true, - SVG: false - }, - - redraw: function () { - if (this._map) { - this.projectLatlngs(); - this._requestUpdate(); - } - return this; - }, - - setStyle: function (style) { - L.setOptions(this, style); - - if (this._map) { - this._updateStyle(); - this._requestUpdate(); - } - return this; - }, - - onRemove: function (map) { - map - .off('viewreset', this.projectLatlngs, this) - .off('moveend', this._updatePath, this); - - if (this.options.clickable) { - this._map.off('click', this._onClick, this); - this._map.off('mousemove', this._onMouseMove, this); - } - - this._requestUpdate(); - }, - - _requestUpdate: function () { - if (this._map && !L.Path._updateRequest) { - L.Path._updateRequest = L.Util.requestAnimFrame(this._fireMapMoveEnd, this._map); - } - }, - - _fireMapMoveEnd: function () { - L.Path._updateRequest = null; - this.fire('moveend'); - }, - - _initElements: function () { - this._map._initPathRoot(); - this._ctx = this._map._canvasCtx; - }, - - _updateStyle: function () { - var options = this.options; - - if (options.stroke) { - this._ctx.lineWidth = options.weight; - this._ctx.strokeStyle = options.color; - } - if (options.fill) { - this._ctx.fillStyle = options.fillColor || options.color; - } - }, - - _drawPath: function () { - var i, j, len, len2, point, drawMethod; - - this._ctx.beginPath(); - - for (i = 0, len = this._parts.length; i < len; i++) { - for (j = 0, len2 = this._parts[i].length; j < len2; j++) { - point = this._parts[i][j]; - drawMethod = (j === 0 ? 'move' : 'line') + 'To'; - - this._ctx[drawMethod](point.x, point.y); - } - // TODO refactor ugly hack - if (this instanceof L.Polygon) { - this._ctx.closePath(); - } - } - }, - - _checkIfEmpty: function () { - return !this._parts.length; - }, - - _updatePath: function () { - if (this._checkIfEmpty()) { return; } - - var ctx = this._ctx, - options = this.options; - - this._drawPath(); - ctx.save(); - this._updateStyle(); - - if (options.fill) { - ctx.globalAlpha = options.fillOpacity; - ctx.fill(); - } - - if (options.stroke) { - ctx.globalAlpha = options.opacity; - ctx.stroke(); - } - - ctx.restore(); - - // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature - }, - - _initEvents: function () { - // TODO dblclick - this._map.on('mousemove', this._onMouseMove, this); - this._map.on('click', this._onClick, this); - }, - - _onClick: function (e) { - if (this._containsPoint(e.layerPoint)) { - this.fire('click', e); - } - }, - - _onMouseMove: function (e) { - if (!this._map || this._map._animatingZoom) { return; } - - // TODO don't do on each move - if (this._containsPoint(e.layerPoint)) { - this._ctx.canvas.style.cursor = 'pointer'; - this._mouseInside = true; - this.fire('mouseover', e); - - } else if (this._mouseInside) { - this._ctx.canvas.style.cursor = ''; - this._mouseInside = false; - this.fire('mouseout', e); - } - } -}); - -L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {} : { - _initPathRoot: function () { - var root = this._pathRoot, - ctx; - - if (!root) { - root = this._pathRoot = document.createElement('canvas'); - root.style.position = 'absolute'; - ctx = this._canvasCtx = root.getContext('2d'); - - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - - this._panes.overlayPane.appendChild(root); - - if (this._zoomAnimated) { - this._pathRoot.className = 'leaflet-zoom-animated'; - this.on('zoomanim', this._animatePathZoom); - this.on('zoomend', this._endPathZoom); - } - this.on('moveend', this._updateCanvasViewport); - this._updateCanvasViewport(); - } - }, - - _updateCanvasViewport: function () { - // don't redraw while zooming. See _updateSvgViewport for more details - if (this._pathZooming) { return; } - this._updatePathViewport(); - - var vp = this._pathViewport, - size = vp.getSize(), - root = this._pathRoot; - - //TODO check if this works properly on mobile webkit - L.DomUtil.setPosition(root, vp.min); - root.width = size.x; - root.height = size.y; - root.getContext('2d').translate(-vp.min.x, -vp.min.y); - } -}); diff --git a/src/layer/vector/canvas/Polygon.Canvas.js b/src/layer/vector/canvas/Polygon.Canvas.js deleted file mode 100644 index 34513511..00000000 --- a/src/layer/vector/canvas/Polygon.Canvas.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Extends L.Polygon to be able to manually detect clicks on Canvas-rendered polygons. - */ - -L.Polygon.include(!L.Path.CANVAS ? {} : { - _containsPoint: function (p) { - var inside = false, - part, p1, p2, - i, j, k, - len, len2; - - // TODO optimization: check if within bounds first - - if (L.Polyline.prototype._containsPoint.call(this, p, true)) { - // click on polygon border - return true; - } - - // ray casting algorithm for detecting if point is in polygon - - for (i = 0, len = this._parts.length; i < len; i++) { - part = this._parts[i]; - - for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { - p1 = part[j]; - p2 = part[k]; - - if (((p1.y > p.y) !== (p2.y > p.y)) && - (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { - inside = !inside; - } - } - } - - return inside; - } -}); diff --git a/src/layer/vector/canvas/Polyline.Canvas.js b/src/layer/vector/canvas/Polyline.Canvas.js deleted file mode 100644 index 7ecce3b6..00000000 --- a/src/layer/vector/canvas/Polyline.Canvas.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Extends L.Polyline to be able to manually detect clicks on Canvas-rendered polylines. - */ - -L.Polyline.include(!L.Path.CANVAS ? {} : { - _containsPoint: function (p, closed) { - var i, j, k, len, len2, dist, part, - w = this.options.weight / 2; - - if (L.Browser.touch) { - w += 10; // polyline click tolerance on touch devices - } - - for (i = 0, len = this._parts.length; i < len; i++) { - part = this._parts[i]; - for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { - if (!closed && (j === 0)) { - continue; - } - - dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]); - - if (dist <= w) { - return true; - } - } - } - return false; - } -}); diff --git a/src/layer/vector2/Canvas.js b/src/layer/vector2/Canvas.js index 08785eb6..103896e2 100644 --- a/src/layer/vector2/Canvas.js +++ b/src/layer/vector2/Canvas.js @@ -40,13 +40,21 @@ L.Canvas = L.Renderer.extend({ this.on('redraw', layer._updatePath, layer); if (layer.options.clickable) { - this._initEvents(layer); + L.DomEvent + .on(this._container, 'mousemove', this._onMouseMove, layer) + .on(this._container, 'click', this._onClick, layer); } }, _addPath: L.Util.falseFn, _removePath: function (layer) { + if (layer.options.clickable) { + L.DomEvent + .off(this._container, 'mousemove', this._onMouseMove, layer) + .off(this._container, 'click', this._onClick, layer); + } + this.off('redraw', layer._updatePath, layer); this._requestRedraw(); }, @@ -109,17 +117,33 @@ L.Canvas = L.Renderer.extend({ // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature }, - // _bringToFront: function (layer) { - // // TODO - // }, + _onClick: function (e) { + console.log(e); + var point = this._map.mouseEventToLayerPoint(e); + if (this._containsPoint(point)) { + this._onMouseClick(e); + } + }, - // _bringToBack: function (layer) { - // // TODO - // }, + _onMouseMove: function (e) { + if (!this._map || this._map._animatingZoom) { return; } - // _initEvents: function (layer) { - // // TODO - // } + var point = this._map.mouseEventToLayerPoint(e); + + // TODO don't do on each move + if (this._containsPoint(point)) { + this._renderer._container.style.cursor = 'pointer'; + this._mouseInside = true; + this._fireMouseEvent(e, 'mouseover'); + + } else if (this._mouseInside) { + this._renderer._container.style.cursor = ''; + this._mouseInside = false; + this._fireMouseEvent(e, 'mouseout'); + } + } + + // TODO _bringToFront & _bringToBack }); L.Browser.canvas = (function () { @@ -129,3 +153,57 @@ L.Browser.canvas = (function () { L.canvas = function () { return new L.Canvas(); }; + + +L.Polyline.prototype._containsPoint = function (p, closed) { + var i, j, k, len, len2, part, + w = (this.options.stroke ? this.options.weight / 2 : 0) + (L.Browser.touch ? 10 : 0); + + for (i = 0, len = this._parts.length; i < len; i++) { + part = this._parts[i]; + + for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { + if (!closed && (j === 0)) { continue; } + + if (L.LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) { + return true; + } + } + } + return false; +}; + +L.Polygon.prototype._containsPoint = function (p) { + var inside = false, + part, p1, p2, i, j, k, len, len2; + + // TODO optimization: check if within bounds first + + // click on polygon border + if (L.Polyline.prototype._containsPoint.call(this, p, true)) { return true; } + + // ray casting algorithm for detecting if point is in polygon + for (i = 0, len = this._parts.length; i < len; i++) { + part = this._parts[i]; + + for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { + p1 = part[j]; + p2 = part[k]; + + if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { + inside = !inside; + } + } + } + + return inside; +}; + +/* +L.Circle.prototype._containsPoint = function (p) { + var center = this._point, + w2 = this.options.stroke ? this.options.weight / 2 : 0; + + return (p.distanceTo(center) <= this._radius + w2); +}; +*/ diff --git a/src/layer/vector2/Path.js b/src/layer/vector2/Path.js index 88e74604..9b636e00 100644 --- a/src/layer/vector2/Path.js +++ b/src/layer/vector2/Path.js @@ -82,25 +82,27 @@ L.Path = L.Layer.extend({ this._fireMouseEvent(e); }, - _fireMouseEvent: function (e) { - if (!this.hasEventListeners(e.type)) { return; } + _fireMouseEvent: function (e, type) { + type = type || e.type; + + if (!this.hasEventListeners(type)) { return; } var map = this._map, containerPoint = map.mouseEventToContainerPoint(e), layerPoint = map.containerPointToLayerPoint(containerPoint), latlng = map.layerPointToLatLng(layerPoint); - this.fire(e.type, { + this.fire(type, { latlng: latlng, layerPoint: layerPoint, containerPoint: containerPoint, originalEvent: e }); - if (e.type === 'contextmenu') { + if (type === 'contextmenu') { L.DomEvent.preventDefault(e); } - if (e.type !== 'mousemove') { + if (type !== 'mousemove') { L.DomEvent.stopPropagation(e); } } diff --git a/src/layer/vector2/SVG.js b/src/layer/vector2/SVG.js index 8fdac112..81285ed5 100644 --- a/src/layer/vector2/SVG.js +++ b/src/layer/vector2/SVG.js @@ -3,6 +3,7 @@ L.SVG = L.Renderer.extend({ onAdd: function () { var container = this._container = L.SVG.create('svg'); + container.setAttribute('pointer-events', 'none'); if (this._zoomAnimated) { L.DomUtil.addClass(container, 'leaflet-zoom-animated'); @@ -94,11 +95,7 @@ L.SVG = L.Renderer.extend({ path.setAttribute('fill', 'none'); } - if (options.pointerEvents) { - path.setAttribute('pointer-events', options.pointerEvents); - } else if (!options.clickable) { - path.setAttribute('pointer-events', 'none'); - } + path.setAttribute('pointer-events', options.pointerEvents || (options.clickable ? 'auto' : 'none')); }, _updatePoly: function (layer, closed) {