Reputation: 597
I'm trying to create a custom Leaflet layer that shall enable the usage of the GoJS library. I've manged most of my main problems such as:
But I'm stuck with the problem of resizing the the nodes while zooming. I'm calculating a scaleFactor
and change the location
of the nodes. The approach works so far, but as far as the map is zoomed out to level 0
and the user zooms back into the location is calculated incorrect. The location of the y-axis is completly wrong. I've also set up a fiddle so you can easily play arroud with the source.
(function () {
if (typeof(L) !== 'undefined' && typeof(go) !== 'undefined') {
L.GoJsLayer = L.Class.extend({
includes: [L.Mixin.Events],
options: {
"animationManager.isEnabled": false,
allowZoom: false,
allowHorizontalScroll: false,
hasHorizontalScrollbar: false,
allowVerticalScroll: false,
hasVerticalScrollbar: false,
padding: 0
},
initialize: function (options) {
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map;
if (!this.diagram) {
this._initDiagram();
}
this._map
.on('viewreset', this._reset, this)
.on('moveend', this._updateViewport, this);
},
onRemove: function (map) {
this._map
.getPanes()
.overlayPane
.removeChild(this._el);
this._map
.off('moveend', this._updateViewport, this);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
_initDiagram: function () {
this._initElement();
this._viewport = this._map.getBounds();
this.diagram = new go.Diagram(
this._el.getAttribute('id')
);
this._setFixedBounds();
this.diagram.setProperties(this.options);
this._setCanvas();
},
_initElement: function () {
var size = this._map.getSize();
this._el = L
.DomUtil
.create('div', 'leaflet-layer');
this._el.setAttribute(
'id',
'leaflet-gojs-diagram-' + L.Util.stamp(this)
);
this._el
.setAttribute('style', this._getElementStyle());
L.DomUtil.addClass(this._el, 'leaflet-zoom-hide');
this._map
.getPanes()
.overlayPane
.appendChild(this._el);
},
_getElementStyle: function (options) {
var size = this._map.getSize(),
paneTranslation,
vpOffset,
translation;
if (this._canvas) {
// This is a dirty solution due to the pressure of time.
// This needs to be refractored!
paneTranslation = L.DomUtil
.getStyle(this._map.getPanes()
.mapPane, 'transform')
.match(/\-?\d+px/g)
.map(function (value) {
return parseInt(value);
});
vpOffset = L.point(paneTranslation[0], paneTranslation[1]);
translation = L
.DomUtil
.getTranslateString(vpOffset.multiplyBy(-1));
return ''
.concat('width: ' + size.x + 'px;')
.concat('height: ' + size.y + 'px;')
.concat('transform: ' + translation);
} else {
translation = L.DomUtil.getTranslateString(L.point(0, 0));
return ''
.concat('width: ' + size.x + 'px;')
.concat('height: ' + size.y + 'px;')
.concat('transform: ' + translation);
}
},
_setFixedBounds: function () {
var width = parseInt(L.DomUtil.getStyle(this._el, 'width')),
height = parseInt(L.DomUtil.getStyle(this._el, 'height'));
this.diagram.setProperties({
fixedBounds: new go.Rect(0, 0, width, height)
});
},
_setCanvas: function () {
var canvasElements = this._el.getElementsByTagName('canvas');
if (canvasElements.length) {
this._canvas = canvasElements.item(0);
return true;
}
return false;
},
_reset: function () {
this._resizeNodes();
},
_resizeNodes: function () {
var scale = this._map.options.crs.scale,
currentScale = scale(this._map.getZoom()),
previousScale = scale(this._calcPreviousScale()),
scaleFactor = currentScale / previousScale;
this.diagram.startTransaction('reposition');
this.diagram.nodes.each(this._resizeNode.bind(this, scaleFactor));
this.diagram.commitTransaction('reposition');
},
_calcPreviousScale: function () {
var vp = this._viewport,
vpNw = vp.getNorthWest(),
vpSw = vp.getSouthWest(),
mb = this._map.getBounds(),
mbNw = mb.getNorthWest(),
mbSw = mb.getSouthWest(),
currentScale = this._map.getZoom(),
previousScale;
if (mbNw.distanceTo(mbSw) > vpNw.distanceTo(vpSw)) {
previousScale = currentScale + 1;
} else {
previousScale = currentScale - 1;
}
return previousScale;
},
_resizeNode: function (scaleFactor, node) {
node.location = new go.Point(
node.location.x * scaleFactor,
node.location.y * scaleFactor
);
},
_updateViewport: function (options) {
this._el.setAttribute('style', this._getElementStyle(options));
this._setFixedBounds();
this._repositionNodes();
this._viewport = this._map.getBounds();
},
_repositionNodes: function () {
this.diagram.startTransaction('reposition');
this.diagram.nodes.each(this._repositionNode.bind(this));
this.diagram.commitTransaction('reposition');
},
_repositionNode: function (node) {
var vp = this._viewport,
vpNw = vp.getNorthWest(),
vpOffset = this._map.latLngToContainerPoint(vpNw),
vpOffsetInverse = vpOffset.multiplyBy(-1),
newX = node.location.x - vpOffsetInverse.x,
newY = node.location.y - vpOffsetInverse.y;
node.location = new go.Point(newX, newY);
}
});
L.goJsLayer = function (options) {
return new L.GoJsLayer(options);
};
}
}());
var $ = go.GraphObject.make,
nodeTemplate,
linkTemplate,
model,
canvasLayer,
map;
// the node template describes how each Node should be constructed
nodeTemplate = $(go.Node, 'Auto',
$(go.Shape, 'Rectangle',
{
fill: '#FFF',
width: 10,
height: 10
}
),
new go.Binding('location', 'loc', go.Point.parse)
);
// the linkTemplates describes how each link should be constructed
linkTemplate = $(go.Link, $(go.Shape));
// the Model holds only the essential information describing the diagram
model = new go.GraphLinksModel(
[
{ key: 1, loc: '320 100' },
{ key: 2, loc: '320 300' }
],
[
{ from: 1, to: 2 }
]
);
// Caution: The model property has to be set after the template properties
canvasLayer = L.goJsLayer({
nodeTemplate: nodeTemplate,
linkTemplate: linkTemplate,
model: model
});
map = L.map('map', {
zoom: 4,
center: [51.505, -0.09],
layers: [
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {noWrap: true}),
canvasLayer
],
//dragging: false
});
html, body, .map {
padding: 0px;
margin: 0px;
height: 100%;
}
div canvas {
outline: none;
}
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css" />
<script type="text/javascript" src="http://gojs.net/latest/release/go-debug.js"></script>
<script type="text/javascript" src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"></script>
<div id="map" class="map"></div>
Upvotes: 1
Views: 1025
Reputation: 597
Thanks a lot for your contributions IvanSanchez. I've solved my issue due to some help in the GoJS-Forum. I've optimised my code using GoJS data binding. In addition to that I've updated my ugly approach to determine the translation using the private _getMapPanePos
method.
Fiddle: http://jsfiddle.net/5x0vtk81/
(function () {
if (typeof(L) !== 'undefined' && typeof(go) !== 'undefined') {
L.GoJsLayer = L.Class.extend({
includes: [L.Mixin.Events],
options: {
"animationManager.isEnabled": false,
allowZoom: false,
allowHorizontalScroll: false,
hasHorizontalScrollbar: false,
allowVerticalScroll: false,
hasVerticalScrollbar: false,
padding: 0
},
initialize: function (options) {
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map;
if (!this.diagram) {
this._initDiagram();
}
this._map
.on('viewreset', this._reset, this)
.on('moveend', this._updateViewport, this);
},
onRemove: function (map) {
this._map
.getPanes()
.overlayPane
.removeChild(this._el);
this._map
.off('moveend', this._updateViewport, this);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
_initDiagram: function () {
this._initElement();
this.diagram = new go.Diagram(
this._el.getAttribute('id')
);
this._setFixedBounds();
this.diagram.setProperties(this.options);
this._setCanvas();
},
_initElement: function () {
var size = this._map.getSize();
this._el = L
.DomUtil
.create('div', 'leaflet-layer');
this._el.setAttribute(
'id',
'leaflet-gojs-diagram-' + L.Util.stamp(this)
);
this._el
.setAttribute('style', this._getElementStyle());
L.DomUtil.addClass(this._el, 'leaflet-zoom-hide');
this._map
.getPanes()
.overlayPane
.appendChild(this._el);
},
_getElementStyle: function (options) {
var size = this._map.getSize(),
panePosition,
transform;
if (this._canvas) {
panePosition = this._map._getMapPanePos();
transform = L
.DomUtil
.getTranslateString(panePosition.multiplyBy(-1));
} else {
transform = L
.DomUtil
.getTranslateString(L.point(0, 0));
}
return L.Util.template(
'width: {width}px; ' +
'height: {height}px; ' +
'transform: {transform}',
{
width: size.x,
height: size.y,
transform: transform
}
);
},
_setFixedBounds: function () {
var width = parseInt(L.DomUtil.getStyle(this._el, 'width')),
height = parseInt(L.DomUtil.getStyle(this._el, 'height'));
this.diagram.setProperties({
fixedBounds: new go.Rect(0, 0, width, height)
});
},
_setCanvas: function () {
var canvasElements = this._el.getElementsByTagName('canvas');
if (canvasElements.length) {
this._canvas = canvasElements.item(0);
return true;
}
return false;
},
_reset: function () {
this.diagram.updateAllTargetBindings('latlong')
},
_updateViewport: function (options) {
this._el.setAttribute('style', this._getElementStyle(options));
this._setFixedBounds();
this.diagram.updateAllTargetBindings('latlong');
}
});
L.goJsLayer = function (options) {
return new L.GoJsLayer(options);
};
}
}());
var $ = go.GraphObject.make,
map,
calcDiagramLocation,
nodeTemplate,
linkTemplate,
model,
canvasLayer;
map = L.map('map', {
zoom: 4,
center: [51.505, -0.09],
layers: [
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {noWrap: true})
],
dragging: true
});
calcDiagramLocation = function(map, data) {
var point = map.latLngToContainerPoint(data);
return new go.Point(point.x, point.y);
};
// the node template describes how each Node should be constructed
nodeTemplate = $(go.Node, 'Auto',
$(go.Shape, 'Rectangle',
{
fill: '#FFF',
width: 10,
height: 10
}
),
new go.Binding('location', 'latlong', calcDiagramLocation.bind(this, map))
);
// the linkTemplates describes how each link should be constructed
linkTemplate = $(go.Link, $(go.Shape));
// the Model holds only the essential information describing the diagram
model = new go.GraphLinksModel(
[
{ key: 1, latlong: [51.507884, -0.087765] }, // london bridge
{ key: 2, latlong: [48.853039, 2.349952] }, // Notre-Dame cathedral
],
[
{ from: 1, to: 2 }
]
);
// Caution: The model property has to be set after the template properties
canvasLayer = L.goJsLayer({
nodeTemplate: nodeTemplate,
linkTemplate: linkTemplate,
model: model
}).addTo(map);
html, body, .map {
padding: 0px;
margin: 0px;
height: 100%;
}
div canvas {
outline: none;
}
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css" />
<script type="text/javascript" src="http://gojs.net/latest/release/go-debug.js"></script>
<script type="text/javascript" src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"></script>
<div id="map" class="map"></div>
Upvotes: 0
Reputation: 19069
// This is a dirty solution due to the pressure of time.
// This needs to be refractored!
paneTranslation = L.DomUtil
.getStyle(this._map.getPanes()
.mapPane, 'transform')
.match(/\-?\d+px/g)
Whoa. It is indeed dirty :-O
I've developed a few Leaflet plugins myself, and my advice is to rely much heavier on methods like map.latLngToPoint()
(for the points) and map.getPixelOrigin()
(for the viewport) - working with absolute coordinates will help. Right now you're basing all the positioning on map.latLngToContainerPoint()
, which implicitly uses getPixelOrigin()
, which changes every time the user pans and zooms, which is making you lose control.
If you will insist on working with pixel coordinates for initializing your graph, project them into LatLng
s with map.project()
when initializing your graph. That way they will get properly reprojected when moving and zooming.
If you want Leaflet for the pan-and-zoom capabilities and not the cartographic capabilities, consider using CRS.Simple
to use a simple non-geographic cartesian coordinate system instead.
Upvotes: 2