implement Canvas events and SVG click-through

This commit is contained in:
Vladimir Agafonkin 2013-12-13 08:49:45 -05:00
parent 41a576b930
commit 6f9d05fc40
9 changed files with 100 additions and 291 deletions

View File

@ -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.'
},

View File

@ -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);

View File

@ -1,9 +0,0 @@
/*
* CircleMarker canvas specific drawing parts.
*/
L.CircleMarker.include(!L.Path.CANVAS ? {} : {
_updateStyle: function () {
L.Path.prototype._updateStyle.call(this);
}
});

View File

@ -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);
}
});

View File

@ -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;
}
});

View File

@ -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;
}
});

View File

@ -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);
};
*/

View File

@ -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);
}
}

View File

@ -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) {