From 8b98f5acc30cec95bf01df39506e4f6721901e3b Mon Sep 17 00:00:00 2001 From: euzuro Date: Fri, 1 Aug 2008 00:17:59 +0000 Subject: [PATCH] ordering . you can now gracefully z-order your vectors... and you can even 'yOrder' them and add background images, making for a nice 3dish look. be sure to check out the two new example html's: marker-shadow.html and ordering.html. Big thanks to tcoulter (funkyc) for a prolonged effort with this patch. It has come a long way and now what a beautiful finish. (Closes #1357) git-svn-id: http://svn.openlayers.org/trunk/openlayers@7652 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf --- examples/marker-shadow.html | 141 ++++++++ examples/marker_shadow.png | Bin 0 -> 374 bytes examples/ordering.html | 203 ++++++++++++ lib/OpenLayers/Layer/Vector.js | 13 +- lib/OpenLayers/Renderer/Elements.js | 476 +++++++++++++++++++++++++--- tests/Renderer/Elements.html | 325 ++++++++++++------- 6 files changed, 996 insertions(+), 162 deletions(-) create mode 100644 examples/marker-shadow.html create mode 100755 examples/marker_shadow.png create mode 100644 examples/ordering.html diff --git a/examples/marker-shadow.html b/examples/marker-shadow.html new file mode 100644 index 0000000000..957854f84f --- /dev/null +++ b/examples/marker-shadow.html @@ -0,0 +1,141 @@ + + + + + + + + + +

Marker Shadows using Background Graphics/Z-Indexes

+ +
+
+ +

+ This example shows off marker shadows using background graphics and z-indexes. Move the features around to show the shadows' interaction. +

+ +
+ + + + + + + + + +
+
+
+
+ The features in this map were generated at random. Each of these features have a backgroundGraphic property set in the style map to add a shadow image. Note that the background graphics are not duplicated features with a different style. +

+ The shadows were set to have a different z-index than the markers themselves, using the backgroundGraphicZIndex property. This makes sure all shadows stay behind the markers, keeping a clean look. The shadows were also placed nicely relative to the external graphic using the backgroundXOffset and backgroundYOffset property. +

+ Y-ordering on the layer is enabled. See the ordering example. +
+
+ +
+ + + + diff --git a/examples/marker_shadow.png b/examples/marker_shadow.png new file mode 100755 index 0000000000000000000000000000000000000000..a5afa6edd9e4a5a525af486f26d962539412d838 GIT binary patch literal 374 zcmV-+0g3*JP)&00004XF*Lt007q5 z)K6G40000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz2uVaiRCwBq zk_~TyKny_TD59Gv`u~69;-*eM6l|AwNk?G2CNFR)JlfK1l^#JUlv3F$8%4mPSMJJH z5@Z=KnrH`a%38_E8&9Mn3DuQ|4oc-J49H>#=jJbIj=*F`#OoQE?X} zRf2E?TcJj|AiHoL#I3_SE$^9Wm=Q%;$4;8)i~F9+e`Gm}U9&zV6-LQ`s7KZy`R3Y@ zAN|5cVa-n(dGO&xMvJg@`Cl?Gt@z8qeYtgX$fIO%UCf3!+099OCC~pP(|8Fm0O + + + + + + + + +

Z-Index/Y-Order Example

+ +
+
+ +

+ This example shows the use of z-indexing and y-ordering of external graphics. Zoom in and out to see this behavior. +

+ +

Z-Index (with Y-Ordering enabled)

+ + + + + +
+
+
+
+ In this map, the gold features all have the same z-index, and the red features have alternating z-indeces. The gold features' z-index is greater than the red features' z-indeces, which is why gold features look to be drawn on top of the red features. Since each gold feature has the same z-index, gold features succomb to y-ordering: this is where features that seem closest to the viewer (lower lattitude) show up above those that seem farther away (higher lattitude). +

+ All vector layers have z-indexing enabled by default, but are not enabled with y-ordering. You can enable y-ordering by passing the parameter yOrdering: true in the vector layer's options hash. For all configurations, if features have the same z-index -- and if y-ordering is enabled: the same lattitude -- those features will succomb to drawing order, where the last feature to be drawn will appear above the rest. +
+
+
+

Drawing Order (no Z-Indexes set, and Y-Ordering disabled)

+ + + + + +
+
+
+
+ In this map, features are not given z-indexes, and the layer's yOrdering parameter is set to the default (false). This configuration makes features succomb to drawing order instead of z-index order or y-order. +

+ The features in this map were drawn from left to right and bottom to top, diagonally, to show that y-ordering is not enabled. +
+
+ + + + diff --git a/lib/OpenLayers/Layer/Vector.js b/lib/OpenLayers/Layer/Vector.js index 565568e53d..4eb2a9ede7 100644 --- a/lib/OpenLayers/Layer/Vector.js +++ b/lib/OpenLayers/Layer/Vector.js @@ -152,6 +152,13 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { * {} */ renderer: null, + + /** + * APIProperty: yOrdering + * {String} Whether or not externalGraphic y-ordering is enabled on this + * layer. Default is false. + */ + yOrdering: false, /** * APIProperty: geometryType @@ -252,8 +259,8 @@ OpenLayers.Layer.Vector = OpenLayers.Class(OpenLayers.Layer, { for (var i=0, len=this.renderers.length; i} This is an array of node id's stored in the + * order that they should show up on screen. Id's higher up in the + * array (higher array index) represent nodes with higher z-indeces. + */ + order: null, + + /** + * Property: indices + * {Object} This is a hash that maps node ids to their z-index value + * stored in the indexer. This is done to make finding a nodes z-index + * value O(1). + */ + indices: null, + + /** + * Property: compare + * {Function} This is the function used to determine placement of + * of a new node within the indexer. If null, this defaults to to + * the Z_ORDER_DRAWING_ORDER comparison method. + */ + compare: null, + + /** + * APIMethod: initialize + * Create a new indexer with + * + * Parameters: + * yOrdering - {Boolean} Whether to use y-ordering. + */ + initialize: function(yOrdering) { + + this.compare = yOrdering ? + OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER_Y_ORDER : + OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER_DRAWING_ORDER; + + this.order = []; + this.indices = {}; + this.maxZIndex = 0; + }, + + /** + * APIMethod: insert + * Insert a new node into the indexer. In order to find the correct + * positioning for the node to be inserted, this method uses a binary + * search. This makes inserting O(log(n)). + * + * Parameters: + * newNode - {DOMElement} The new node to be inserted. + * root - {DOMElement} The root node from which to insert the new node. + */ + insert: function(newNode, root) { + var nodeId = newNode.id; + + this.determineZIndex(newNode); + + var leftIndex = -1; + var rightIndex = this.order.length; + var middle; + + while (rightIndex - leftIndex > 1) { + middle = parseInt((leftIndex + rightIndex) / 2); + + var nextId = this.order[middle]; + var nextNode = OpenLayers.Util.getElement(nextId); + + var placement = this.compare(this, newNode, nextNode); + + if (placement > 0) { + leftIndex = middle; + } else { + rightIndex = middle; + } + } + + this.order.splice(rightIndex, 0, nodeId); + this.indices[nodeId] = this.getZIndex(newNode); + + // If the new node should be before another in the index + // order, insert the new node before the next; else, lets just + // append the new one on the end, making it the highest in the index order. + var nextIndex = rightIndex + 1; + if (nextIndex < this.order.length) { + var nextNode = OpenLayers.Util.getElement(this.order[nextIndex]); + root.insertBefore(newNode, nextNode); + } else { + root.appendChild(newNode); + } + }, + + /** + * APIMethod: remove + * + * Parameters: + * node - {DOMElement} The node to be removed. + */ + remove: function(node) { + var nodeId = node.id; + var arrayIndex = OpenLayers.Util.indexOf(this.order, nodeId); + if (arrayIndex >= 0) { + // Remove it from the order array, as well as deleting the node + // from the indeces hash. + this.order.splice(arrayIndex, 1); + delete this.indices[nodeId]; + + // Reset the maxium z-index based on the last item in the + // order array. + var lastId = this.order[this.order.length - 1]; + this.maxZIndex = this.indices[lastId]; + } + }, + + /** + * APIMethod: exists + * + * Parameters: + * node- {DOMElement} The node to test for existence. + * + * Returns: + * {Boolean} Whether or not the node exists in the indexer? + */ + exists: function(node) { + return (this.indices[node.id] != null); + }, + + /** + * APIMethod: getZIndex + * Get the z-index value for the current node from the node data itself. + * + * Parameters: + * node - {DOMElement} The node whose z-index to get. + * + * Returns: + * {Integer} The z-index value for the specified node (from the node + * data itself). + */ + getZIndex: function(node) { + return node._style.graphicZIndex; + }, + + /** + * Method: determineZIndex + * Determine the z-index for the current node if there isn't one, + * and set the maximum value if we've found a new maximum. + * + * Parameters: + * node - {DOMElement} + */ + determineZIndex: function(node) { + var zIndex = node._style.graphicZIndex; + + // Everything must have a zIndex. If none is specified, + // this means the user *must* (hint: assumption) want this + // node to succomb to drawing order. To enforce drawing order + // over all indexing methods, we'll create a new z-index that's + // greater than any currently in the indexer. + if (zIndex == null) { + zIndex = this.maxZIndex; + node._style.graphicZIndex = zIndex; + } else if (zIndex > this.maxZIndex) { + this.maxZIndex = zIndex; + } + }, + + CLASS_NAME: "OpenLayers.ElementsIndexer" +}); + +/** + * Namespace: OpenLayers.ElementsIndexer.IndexingMethods + * These are the compare methods for figuring out where a new node should be + * placed within the indexer. These methods are very similar to general + * sorting methods in that they return -1, 0, and 1 to specify the + * direction in which new nodes fall in the ordering. + */ +OpenLayers.ElementsIndexer.IndexingMethods = { + + /** + * Method: Z_ORDER + * This compare method is used by other comparison methods. + * It can be used individually for ordering, but is not recommended, + * because it doesn't subscribe to drawing order. + * + * Parameters: + * indexer - {} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER: function(indexer, newNode, nextNode) { + var newZIndex = indexer.getZIndex(newNode); + + var returnVal = 0; + if (nextNode) { + var nextZIndex = indexer.getZIndex(nextNode); + returnVal = newZIndex - nextZIndex; + } + + return returnVal; + }, + + /** + * APIMethod: Z_ORDER_DRAWING_ORDER + * This method orders nodes by their z-index, but does so in a way + * that, if there are other nodes with the same z-index, the newest + * drawn will be the front most within that z-index. This is the + * default indexing method. + * + * Parameters: + * indexer - {} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER_DRAWING_ORDER: function(indexer, newNode, nextNode) { + var returnVal = OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER( + indexer, + newNode, + nextNode + ); + + // Make Z_ORDER subscribe to drawing order by pushing it above + // all of the other nodes with the same z-index. + if (nextNode && returnVal == 0) { + returnVal = 1; + } + + return returnVal; + }, + + /** + * APIMethod: Z_ORDER_Y_ORDER + * This one should really be called Z_ORDER_Y_ORDER_DRAWING_ORDER, as it + * best describes which ordering methods have precedence (though, the + * name would be too long). This method orders nodes by their z-index, + * but does so in a way that, if there are other nodes with the same + * z-index, the nodes with the lower y position will be "closer" than + * those with a higher y position. If two nodes have the exact same y + * position, however, then this method will revert to using drawing + * order to decide placement. + * + * Parameters: + * indexer - {} + * newNode - {DOMElement} + * nextNode - {DOMElement} + * + * Returns: + * {Integer} + */ + Z_ORDER_Y_ORDER: function(indexer, newNode, nextNode) { + var returnVal = OpenLayers.ElementsIndexer.IndexingMethods.Z_ORDER( + indexer, + newNode, + nextNode + ); + + if (nextNode && returnVal == 0) { + var newLat = newNode._geometry.getBounds().bottom; + var nextLat = nextNode._geometry.getBounds().bottom; + + var result = nextLat - newLat; + returnVal = (result ==0) ? 1 : result; + } + + return returnVal; + } +}; + /** * Class: OpenLayers.Renderer.Elements * This is another virtual class in that it should never be instantiated by @@ -41,6 +327,19 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { */ xmlns: null, + /** + * Property: Indexer + * {} An instance of OpenLayers.ElementsIndexer + * created upon initialization. + */ + indexer: null, + + /** + * Constant: BACKGROUND_ID_SUFFIX + * {String} + */ + BACKGROUND_ID_SUFFIX: "_background", + /** * Property: minimumSymbolizer * {Object} @@ -57,8 +356,9 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { * * Parameters: * containerID - {String} + * yOrdering - {Boolean} Whether or not y-ordering is enabled. */ - initialize: function(containerID) { + initialize: function(containerID, yOrdering) { OpenLayers.Renderer.prototype.initialize.apply(this, arguments); this.rendererRoot = this.createRenderRoot(); @@ -66,6 +366,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { this.rendererRoot.appendChild(this.root); this.container.appendChild(this.rendererRoot); + + this.indexer = new OpenLayers.ElementsIndexer(yOrdering); }, /** @@ -135,22 +437,13 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { return; }; + // + if (style.backgroundGraphic) { + this.redrawBackgroundNode(geometry.id, geometry, style, featureId); + } + if (style.display != "none") { - //first we create the basic node and add it to the root - var nodeType = this.getNodeType(geometry, style); - var node = this.nodeFactory(geometry.id, nodeType); - node._featureId = featureId; - node._geometryClass = geometry.CLASS_NAME; - node._style = style; - - //now actually draw the node, and style it - node = this.drawGeometryNode(node, geometry); - - // append the node to root (but only if it's new) - if (node.parentNode != this.root) { - this.root.appendChild(node); - } - this.postDraw(node); + this.redrawNode(geometry.id, geometry, style, featureId); } else { node = OpenLayers.Util.getElement(geometry.id); if (node) { @@ -158,6 +451,90 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { } } }, + + /** + * Method: redrawNode + * + * Parameters: + * id - {String} + * geometry - {} + * style - {Object} + * featureId - {String} + */ + redrawNode: function(id, geometry, style, featureId) { + // Get the node if it's already on the map. + var currentNode = OpenLayers.Util.getElement(id); + + // Create a new node, or use the current one if it's + // already there. + var newNode; + if (!currentNode) { + var nodeType = this.getNodeType(geometry, style); + newNode = this.createNode(nodeType, id); + } else { + newNode = currentNode; + } + + // Set the data for the node, then draw it. + newNode._featureId = featureId; + newNode._geometry = geometry; + newNode._geometryClass = geometry.CLASS_NAME; + newNode._style = style; + newNode = this.drawGeometryNode(newNode, geometry, style); + + // If the node is known to the indexer, remove it so we can + // recalculate where it should go. + if (this.indexer.exists(newNode)) { + this.indexer.remove(newNode); + } + + // Insert the node into the indexer so it can show us where to place it. + // Note that this operation is O(log(n)). If there's a performance + // problem (when dragging, for instance) this is likely where it + // would be. + this.indexer.insert(newNode, this.root); + + this.postDraw(newNode); + }, + + /** + * Method: redrawBackgroundNode + * Redraws the node using special 'background' style properties. Basically + * just calls redrawNode(), but instead of directly using the + * 'externalGraphic', 'graphicXOffset', 'graphicYOffset', and + * 'graphicZIndex' properties directly from the specified 'style' + * parameter, we create a new style object and set those properties + * from the corresponding 'background'-prefixed properties from + * specified 'style' parameter. + * + * Parameters: + * id - {String} + * geometry - {} + * style - {Object} + * featureId - {String} + */ + redrawBackgroundNode: function(id, geometry, style, featureId) { + var backgroundStyle = OpenLayers.Util.extend({}, style); + + // Set regular style attributes to apply to the background styles. + backgroundStyle.externalGraphic = backgroundStyle.backgroundGraphic; + backgroundStyle.graphicXOffset = backgroundStyle.backgroundXOffset; + backgroundStyle.graphicYOffset = backgroundStyle.backgroundYOffset; + backgroundStyle.graphicZIndex = backgroundStyle.backgroundGraphicZIndex; + + // Erase background styles. + backgroundStyle.backgroundGraphic = null; + backgroundStyle.backgroundXOffset = null; + backgroundStyle.backgroundYOffset = null; + backgroundStyle.backgroundGraphicZIndex = null; + + this.redrawNode( + id + this.BACKGROUND_ID_SUFFIX, + geometry, + backgroundStyle, + null + ); + }, /** * Method: drawGeometryNode @@ -213,7 +590,7 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: postDraw * Things that have do be done after the geometry node is appended - * to its parent node. To be overridden by subclasses. + * to its parent node. To be overridden by subclasses. * * Parameters: * node - {DOMElement} @@ -223,8 +600,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawPoint * Virtual function for drawing Point Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -235,8 +612,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawLineString * Virtual function for drawing LineString Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -247,8 +624,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawLinearRing * Virtual function for drawing LinearRing Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -259,8 +636,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawPolygon * Virtual function for drawing Polygon Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -271,8 +648,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawRectangle * Virtual function for drawing Rectangle Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -283,8 +660,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawCircle * Virtual function for drawing Circle Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -295,8 +672,8 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { /** * Method: drawSurface * Virtual function for drawing Surface Geometry. - * Should be implemented by subclasses. - * This method is only called by the renderer itself. + * Should be implemented by subclasses. + * This method is only called by the renderer itself. * * Parameters: * node - {DOMElement} @@ -312,7 +689,7 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { * * Returns: * {} A geometry from an event that - * happened on a layer + * happened on a layer. */ getFeatureIdFromEvent: function(evt) { var node = evt.target || evt.srcElement; @@ -340,10 +717,24 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { } else { var element = OpenLayers.Util.getElement(geometry.id); if (element && element.parentNode) { + if (element.geometry) { + element.geometry.destroy(); + element.geometry = null; + } element.parentNode.removeChild(element); + + this.indexer.remove(element); + + var backgroundId = geometry.id + this.BACKGROUND_ID_SUFFIX; + var bElem = OpenLayers.Util.getElement(backgroundId); + if (bElem && bElem.parentNode) { + // No need to destroy the geometry since the element and the background + // node share the same geometry. + bElem.parentNode.removeChild(bElem); + } } } - }, + }, /** * Method: nodeFactory @@ -354,10 +745,10 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { * * Parameters: * id - {String} - * type - {String} type Kind of node to draw + * type - {String} type Kind of node to draw. * * Returns: - * {DOMElement} A new node of the given type and id + * {DOMElement} A new node of the given type and id. */ nodeFactory: function(id, type) { var node = OpenLayers.Util.getElement(id); @@ -372,6 +763,19 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { return node; }, + /** + * Method: createNode + * + * Parameters: + * type - {String} Kind of node to draw. + * id - {String} Id for node. + * + * Returns: + * {DOMElement} A new node of the given type and id. + * This function must be overridden by subclasses. + */ + createNode: function(type, id) {}, + /** * Method: isComplexSymbol * Determines if a symbol cannot be rendered using drawCircle @@ -385,7 +789,7 @@ OpenLayers.Renderer.Elements = OpenLayers.Class(OpenLayers.Renderer, { isComplexSymbol: function(graphicName) { return (graphicName != "circle") && !!graphicName; }, - + CLASS_NAME: "OpenLayers.Renderer.Elements" }); diff --git a/tests/Renderer/Elements.html b/tests/Renderer/Elements.html index 0ebeb9f58e..edd71547b7 100644 --- a/tests/Renderer/Elements.html +++ b/tests/Renderer/Elements.html @@ -3,12 +3,9 @@