OpenLayers.Strategy.Cluster Performance beim Reinzoomen

Hallo OpenStreetMap Forum,

das ist mein erster Eintrag hier, ich hoffe ich poste das an richtiger Stelle.

Meine Problem:
Ich beschäftige mich gerade mit dem Clustern von - testweise - 2000 Punkten. Diese lade ich als JSON mit Hilfe von jQuery in die Karte.
Das funktioniert soweit auch prima.
Wenn ich abe in die Karte zoome und die Cluster sich immer weiter aufteilen verzörgert sich mit jeder Zoomstufe der Seitenaufbau immer mehr.

Ist das so, oder mache ich was falsch?

Hier mein Code:

var map;
var base;
var clusters;
var clusterStrat;
var style;
var features=[];

$(document).ready(init); 

function init() {
	map = new OpenLayers.Map('mapdiv');
	base = new OpenLayers.Layer.OSM();
	style = new OpenLayers.Style({
		pointRadius: "${radius}",
		fillColor: "${fill}",
		fillOpacity: 0.8,
		strokeColor: "#cc6633",
		strokeWidth: 1,
		strokeOpacity: 0.8
	}, {
		context: {
			radius: function(feature) {
				var pix = 4;
				if(feature.cluster) {
					pix = Math.min(feature.attributes.count, 7) + 4;
				}
				return pix;
			},
			fill:function(feature) {
				return (feature.attributes.count<=1) ? "#ff0000":"#ffcc66";
			}
		}
	});
	
	clusterStrat=new OpenLayers.Strategy.Cluster({distance: 50});
	clusters = new OpenLayers.Layer.Vector("Cluster", {
        projection: "EPSG:4326",
		strategies: [clusterStrat],
		styleMap: new OpenLayers.StyleMap({
			"default": style,
			"select": {
				fillColor: "#8aeeef",
				strokeColor: "#32a8a9"
			}
		})

	});
	

	map.addLayers([base, clusters]);
	map.setCenter(new OpenLayers.LonLat(8.2,51.9 ).transform(
            new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984
            map.getProjectionObject() // to Spherical Mercator Projection
          ), 7);
	$.getJSON('list.txt', function(data){addToCluster(data)});
	

}

function addToCluster(data){	
	for (var i = 0; i < data.length; i++) {
		var point = new OpenLayers.Geometry.Point(data[i].lng, data[i].lat);
		point.transform( new OpenLayers.Projection("EPSG:4326"),map.getProjectionObject() );
		var feature = new OpenLayers.Feature.Vector(point, {title: 'Titel' +i});
		features.push(feature);
	}
	clusters.addFeatures(features);
}

Und so sieht die Datei list.txt aus (Gekürzt)


[{"lat":51.934643413443155,"lng":8.249464853629885,"data":0},{"lat":47.409539465570454,"lng":13.722000000404648,"data":1},{"lat":52.582941634704724,"lng":9.586924136998208,"data":2}, ...]

Na dann erst mal herzlich willkommen bei uns Chris :slight_smile:

Ich glaube nicht, dass du da wirklich etwas falsch machst, du stößt einfach nur an die Grenzen von JavaScript und der Performance. Eine mögliche Lösung wäre es, die Cluster auf einem Server vorzuhalten und dann als zusätzlichen Layer, anstatt deines eigentlichen Datenlayers für bestimmte Zoomstufen anzuzeigen. So muss sich nicht der Browser um die vielen Berechnungen kümmern und du bewahrst aber trotzdem die Übersicht durch Cluster-Bildung.

Für solch große Daten würde ich unbedingt eine Server-Lösung mit einbauen, damit der Client minimale Arbeit hat.

Erstmal danke für’s willkommen heissen und für die Antwort.

Nur an Javascript kann das aber eigentlich nicht liegen.

Wenn ich in GoogleMaps das gleiche mache gibt es keine Performance-Probleme.
Und bei der gleichen Anzahl von Markern in OpenLayers gibt es auch keine Probleme.

Und ausserdem frage ich mich warum das erst dann schwierig wird wenn ich da reinzoome. Das ganze Array mit den Punkten muss ja auch in kleineren Zoomstufen geparst werden.

Gruß Chris

Mit Google kenne ich mich nun wieder nicht aus :wink:

Hab noch mal in deinen Source geschaut, du fügst die Punkte ja selber hinzu? Du kannst bei OL auch direkt ein GeoJSON Format wählen, dass sollte schon extrem effizient sein.
http://dev.openlayers.org/releases/OpenLayers-2.11/examples/geojson.html

Das Clustern funktioniert ja so, dass er nach dem Zoomen für jeden Punkt eine Bereichssuche durchführen muss um die möglichen Nachbarn zu identifizieren. Das ist schon nicht so einfach, ich weiß aber auch nicht was OL da für Datenstrukturen nutzt, Parsen passiert nur am Anfang, danach wir es AFAIK im Ram gehalten. Bin mir also nicht sicher, ob ein anderer Loader dein Problem lösen würde…

Wenn man die Punkte erst zu Laufzeit holt, muss einfach viel weniger geprüft werden. Und wenn man das Prüfen dann noch an den Server deligiert (der es dann wieder erst bei der Veränderung der Daten durchführen muss), gehts super schnell.

Ich vermute, dass die Karte so langsam ist, weil einfach die Datenmenge zu groß ist. 2000 Punkte sind schon eine ganze Menge und die müssen erstmal in den Browser geladen werden.
Die Karte wäre schneller, wenn du aus einer Datenbank immer nur die Punkte lädst, die gerade auf dem aktuellen Kartenausschnitt angezeigt werden.

Man kommt irgendwann eh nicht mehr drum herum, wenn die Daten mehr werden :wink:
Vielleicht hilft dir meine Applikation, um da durch zu sehen (ist aber WIP):
http://www.opennet-initiative.de/map2/

Ansonsten kannst du auch einen Geoserver+WFS Schnittstelle nehmen, aber ich fand GeoJSON und eine kleine Eigentwicklung in meinem Fall etwas sinnvoller.

Danke, ich werde mir das morgen mal ansehen.

GeoJSON scheint mir aber eine Datei mit vielen Punkten sehr aufzublasen. Da scheint mir das POIS-Format geeigneter zu sein.
Ich beschäftige mich auch erst seit ein paar Tagen mit Openlayers und verstehe z.B. noch nicht ganz wie ich die Koordinaten im POIS- oder GeoJSON-Format in die Karte lade.
Bei WPS und WIP usw, muss ich auch immer erstmal recherchieren um zu verstehen was das ist.

Das ganze serverseitig zu lösen gefällt mir gerade nicht.
Ich will halt vermeiden, dass bei jedem kleinen Verschieben oder Zoomen die Punkte neu geladen werden. Die Anwendung soll irgendwann mal auch auf mobilen Geräten laufen und wenn die Verbndung gerade nicht so dolle ist, verzögert das Laden der Punkte den Aufbau der Karte zusätzlich. Die Datei mit den 2000 Punkten ist auch gerade mal 120kB gross.

Ich habe mal eine Openlayerkarte neben eine Googlekarte gestellt um das Problem zu verdeutlichen.
=> http://www.webxvideo.de/cluster/
Interessanterweise ist die Googlekarte in den kleineren Zoomstufen lahmer und bei Openlayer verhält es sich genau andersherum.

Schönes Beispiel und interessanter Vergleich.

So wie ich das sehe, liegt der Unterschied in der Implementierung des Clusterings:

  • Beim markerclusterer.js für Google Maps werden in “createClusters_” nur die Marker berücksichtigt, die im aktuellen Kartenausschnitt liegen.
  • In OpenLayers.Strategy.Cluster.cluster werden dagegen immer alle Features verarbeitet. Bei steigendem Zoom Level werden es daher immer mehr Cluster und entsprechend mehr Schleifendurchläufe in der cluster Funktion.

Folgende Debug-Ausgabe verdeutlicht das (addFeatures wird innerhalb von cluster aufgerufen):


addFeatures: 44ms
cluster: 44ms
features: 2001, clusters: 7, zoom: 4 
addFeatures: 6ms
cluster: 17ms 
features: 2001, clusters: 20, zoom: 5
addFeatures: 19ms
cluster: 29ms 
features: 2001, clusters: 66, zoom: 6
addFeatures: 58ms
cluster: 85ms 
features: 2001, clusters: 222, zoom: 7
addFeatures: 94ms
cluster: 168ms 
features: 2001, clusters: 661, zoom: 8
addFeatures: 200ms
cluster: 377ms 
features: 2001, clusters: 1326, zoom: 9
addFeatures: 269ms
cluster: 509ms 
features: 2001, clusters: 1770, zoom: 10
addFeatures: 249ms
cluster: 498ms 
features: 2001, clusters: 1937, zoom: 11
addFeatures: 291ms
cluster: 578ms 
features: 2001, clusters: 1982, zoom: 12
addFeatures: 252ms
cluster: 509ms 
features: 2001, clusters: 1995, zoom: 13


Hier der dazu eingefügte Code (vor $(document).ready(init);):


clusterOrig = OpenLayers.Strategy.Cluster.prototype.cluster;
OpenLayers.Strategy.Cluster.prototype.cluster = function(event) {
    console.time("cluster");
    clusterOrig.apply(this, arguments);
    console.timeEnd("cluster");
    console.log("features: " + (this.features && this.features.length) + ", clusters: " + (this.clusters && this.clusters.length) + ", zoom: " + this.layer.map.getZoom());
};

addFeaturesOrig = OpenLayers.Layer.Vector.prototype.addFeatures;
OpenLayers.Layer.Vector.prototype.addFeatures = function(features, options) {
    console.time("addFeatures");
    addFeaturesOrig.apply(this, arguments);
    console.timeEnd("addFeatures");
};

Zur Optimierung müsste man also die Features auf den aktuellen Anzeigebereich begrenzen.

Eine einfache Lösung für bereits geladene Features fällt mir aber gerade nicht ein. Die BBOX Strategy sendet beim HTTP Protocol nur einen Query String, damit eine server-seitige Komponente danach filtern kann. Evtl. könnte man BBOX.triggerRead in einer eigenen Unterklasse überschreiben und die in einer globalen Variable gespeicherten Features selbst filtern. Oder eben gleich die Cluster Strategy entsprechend erweitern/ändern.

Gruß,
Norbert

So, da bin ich wieder und die Lösung war recht einfach.
In der Funktion addToCluster() lösche ich bei jedem Verschieben und Zoomen alle Punkte und lege sie an Hand der Karten-Bounds neu an.
Das ganze funktioniert mit meinen 2000 Testpunkten reibungslos.
=> http://webxvideo.de/cluster/cluster.html

Und hier der Code:

<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="OpenLayers.js"></script>
<script type="text/javascript">
var map;
var base;
var clusters;
var clusterStrat;
var pointList=[];

$(document).ready(init); 

function init() {
	map = new OpenLayers.Map('mapdiv',{eventListeners: {"moveend": addToCluster}});
	base = new OpenLayers.Layer.OSM({maxResolution: 100});
	var style = new OpenLayers.Style({
		pointRadius: "${radius}",
		fillColor: "${fill}",
		fillOpacity: 0.8,
		strokeColor: "#cc6633",
		strokeWidth: 1,
		strokeOpacity: 0.8
	}, {
		context: {
			radius: function(feature) {
				var pix = 4;
				if(feature.cluster) {
					pix = Math.min(feature.attributes.count, 7) + 4;
				}
				return pix;
			},
			fill:function(feature) {
				return (feature.attributes.count<=1) ? "#ff0000":"#ffcc66";
			}
		}
	});
	
	clusterStrat=new OpenLayers.Strategy.Cluster({distance: 50});
	clusters = new OpenLayers.Layer.Vector("Cluster", {
        projection: "EPSG:4326",
		strategies: [clusterStrat],
		styleMap: new OpenLayers.StyleMap({
			"default": style,
			"select": {
				fillColor: "#8aeeef",
				strokeColor: "#32a8a9"
			}
		})

	});
	

	map.addLayers([base, clusters]);
	var center=new OpenLayers.LonLat(8.2,51.9 ).transform(
            new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984
            map.getProjectionObject() // to Spherical Mercator Projection
          );
	map.setCenter(center, 7);
	$.getJSON('list.txt', function(data){onData(data)});
	

}

// ondata wird beim Empfang der JSON-Liste aufgerufen und erstellt das Array mit allen Punkten
function onData(data){
	for (var i = 0; i < data.length; i++) {
		var point = new OpenLayers.Geometry.Point(data[i].lng, data[i].lat);
		point.transform( new OpenLayers.Projection("EPSG:4326"),map.getProjectionObject() );
		pointList.push(point);
	}
	addToCluster();
}
// addToCluster löscht alle vorhandenen Features, überprüft welche Punkte im Kartenbereich liegen und fügt diese neu hinzu.
function addToCluster(){
	clusters.destroyFeatures();
	clusterStrat.deactivate();
	var features=[];
	// Marker im Kartenbereich + radius finden
	var bounds=map.getExtent();
	for (var i = 0; i < pointList.length; i++) { 
		if(pointList[i].x >= bounds.left && pointList[i].x <= bounds.right && pointList[i].y >= bounds.bottom && pointList[i].y <= bounds.top){
			features.push(new OpenLayers.Feature.Vector(pointList[i]));
		}
	}
	clusterStrat.activate();
	clusters.addFeatures(features);
}
</script>

Sehr schön!

Danke für die Rückmeldung und den Code.

Ah ok, also wird er durch die Anzahl der Punkte träge. Vielleicht könnte man da auch mit einer BBOX Strategy arbeiten… Danke fürs Feedback!