Merge pull request #3010 from ahocevar/style-geometry
Allow styles to override feature geometries
This commit is contained in:
75
examples/earthquake-clusters.html
Normal file
75
examples/earthquake-clusters.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1">
|
||||
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
|
||||
<link rel="stylesheet" href="../css/ol.css" type="text/css">
|
||||
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="../resources/layout.css" type="text/css">
|
||||
<link rel="stylesheet" href="../resources/bootstrap/css/bootstrap-responsive.min.css" type="text/css">
|
||||
<title>Earthquake Clusters</title>
|
||||
<style>
|
||||
#map {
|
||||
position: relative;
|
||||
}
|
||||
#info {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
z-index: 100;
|
||||
}
|
||||
.tooltip.in {
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100);
|
||||
}
|
||||
.tooltip.top .tooltip-arrow {
|
||||
border-top-color: white;
|
||||
}
|
||||
.tooltip-inner {
|
||||
border: 2px solid white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<a class="brand" href="./"><img src="../resources/logo.png"> OpenLayers 3 Examples</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row-fluid">
|
||||
|
||||
<div class="span12">
|
||||
<h4 id="title">Earthquake Clusters</h4>
|
||||
<p id="shortdesc">Demonstrates the use of style geometries to render source features of a cluster.</p>
|
||||
<div id="docs">
|
||||
<p>
|
||||
This example parses a KML file and renders the features as clusters on a vector layer. The styling in this example is quite involved. Single earthquake locations (rendered as stars) have a size relative to their magnitude. Clusters have an opacity relative to the number of features in the cluster, and a size that represents the extent of the features that make up the cluster. When clicking or hovering on a cluster, the individual features that make up the cluster will be shown.
|
||||
</p>
|
||||
<p>To achieve this, we make heavy use of style functions and <code>ol.style.Style#geometry</code>. See the <a href="earthquake-clusters.js" target="_blank">earthquake-clusters.js source</a> to see how this is done.</p>
|
||||
</div>
|
||||
<div id="tags">KML, vector, style, geometry, cluster</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="../resources/jquery.min.js" type="text/javascript"></script>
|
||||
<script src="../resources/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
|
||||
<script src="../resources/example-behaviour.js" type="text/javascript"></script>
|
||||
<script src="loader.js?id=earthquake-clusters" type="text/javascript"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
155
examples/earthquake-clusters.js
Normal file
155
examples/earthquake-clusters.js
Normal file
@@ -0,0 +1,155 @@
|
||||
goog.require('ol.Map');
|
||||
goog.require('ol.View');
|
||||
goog.require('ol.extent');
|
||||
goog.require('ol.interaction');
|
||||
goog.require('ol.interaction.Select');
|
||||
goog.require('ol.layer.Tile');
|
||||
goog.require('ol.layer.Vector');
|
||||
goog.require('ol.source.Cluster');
|
||||
goog.require('ol.source.KML');
|
||||
goog.require('ol.source.Stamen');
|
||||
goog.require('ol.style.Circle');
|
||||
goog.require('ol.style.Fill');
|
||||
goog.require('ol.style.RegularShape');
|
||||
goog.require('ol.style.Stroke');
|
||||
goog.require('ol.style.Style');
|
||||
goog.require('ol.style.Text');
|
||||
|
||||
|
||||
var earthquakeFill = new ol.style.Fill({
|
||||
color: 'rgba(255, 153, 0, 0.8)'
|
||||
});
|
||||
var earthquakeStroke = new ol.style.Stroke({
|
||||
color: 'rgba(255, 204, 0, 0.2)',
|
||||
width: 1
|
||||
});
|
||||
var textFill = new ol.style.Fill({
|
||||
color: '#fff'
|
||||
});
|
||||
var textStroke = new ol.style.Stroke({
|
||||
color: 'rgba(0, 0, 0, 0.6)',
|
||||
width: 3
|
||||
});
|
||||
var invisibleFill = new ol.style.Fill({
|
||||
color: 'rgba(255, 255, 255, 0.01)'
|
||||
});
|
||||
|
||||
function createEarthquakeStyle(feature) {
|
||||
// 2012_Earthquakes_Mag5.kml stores the magnitude of each earthquake in a
|
||||
// standards-violating <magnitude> tag in each Placemark. We extract it
|
||||
// from the Placemark's name instead.
|
||||
var name = feature.get('name');
|
||||
var magnitude = parseFloat(name.substr(2));
|
||||
var radius = 5 + 20 * (magnitude - 5);
|
||||
|
||||
return new ol.style.Style({
|
||||
geometry: feature.getGeometry(),
|
||||
image: new ol.style.RegularShape({
|
||||
radius1: radius,
|
||||
radius2: 3,
|
||||
points: 5,
|
||||
angle: Math.PI,
|
||||
fill: earthquakeFill,
|
||||
stroke: earthquakeStroke
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
var maxFeatureCount;
|
||||
function calculateClusterInfo(resolution) {
|
||||
maxFeatureCount = 0;
|
||||
var features = vector.getSource().getFeatures();
|
||||
var feature, radius;
|
||||
for (var i = features.length - 1; i >= 0; --i) {
|
||||
feature = features[i];
|
||||
var originalFeatures = feature.get('features');
|
||||
var extent = ol.extent.createEmpty();
|
||||
for (var j = 0, jj = originalFeatures.length; j < jj; ++j) {
|
||||
ol.extent.extendCoordinate(extent,
|
||||
originalFeatures[j].getGeometry().getCoordinates());
|
||||
}
|
||||
maxFeatureCount = Math.max(maxFeatureCount, jj);
|
||||
radius = 0.25 * (ol.extent.getWidth(extent) + ol.extent.getHeight(extent)) /
|
||||
resolution;
|
||||
feature.set('radius', radius);
|
||||
}
|
||||
}
|
||||
|
||||
var currentResolution;
|
||||
function styleFunction(feature, resolution) {
|
||||
if (resolution != currentResolution) {
|
||||
calculateClusterInfo(resolution);
|
||||
currentResolution = resolution;
|
||||
}
|
||||
var style;
|
||||
var size = feature.get('features').length;
|
||||
if (size > 1) {
|
||||
style = [new ol.style.Style({
|
||||
image: new ol.style.Circle({
|
||||
radius: feature.get('radius'),
|
||||
fill: new ol.style.Fill({
|
||||
color: [255, 153, 0, Math.min(0.8, 0.4 + (size / maxFeatureCount))]
|
||||
})
|
||||
}),
|
||||
text: new ol.style.Text({
|
||||
text: size.toString(),
|
||||
fill: textFill,
|
||||
stroke: textStroke
|
||||
})
|
||||
})];
|
||||
} else {
|
||||
var originalFeature = feature.get('features')[0];
|
||||
style = [createEarthquakeStyle(originalFeature)];
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
function selectStyleFunction(feature, resolution) {
|
||||
var styles = [new ol.style.Style({
|
||||
image: new ol.style.Circle({
|
||||
radius: feature.get('radius'),
|
||||
fill: invisibleFill
|
||||
})
|
||||
})];
|
||||
var originalFeatures = feature.get('features');
|
||||
var originalFeature;
|
||||
for (var i = originalFeatures.length - 1; i >= 0; --i) {
|
||||
originalFeature = originalFeatures[i];
|
||||
styles.push(createEarthquakeStyle(originalFeature));
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
var vector = new ol.layer.Vector({
|
||||
source: new ol.source.Cluster({
|
||||
distance: 40,
|
||||
source: new ol.source.KML({
|
||||
extractStyles: false,
|
||||
projection: 'EPSG:3857',
|
||||
url: 'data/kml/2012_Earthquakes_Mag5.kml'
|
||||
})
|
||||
}),
|
||||
style: styleFunction
|
||||
});
|
||||
|
||||
var raster = new ol.layer.Tile({
|
||||
source: new ol.source.Stamen({
|
||||
layer: 'toner'
|
||||
})
|
||||
});
|
||||
|
||||
var map = new ol.Map({
|
||||
layers: [raster, vector],
|
||||
interactions: ol.interaction.defaults().extend([new ol.interaction.Select({
|
||||
condition: function(evt) {
|
||||
return evt.originalEvent.type == 'mousemove' ||
|
||||
evt.type == 'singleclick';
|
||||
},
|
||||
style: selectStyleFunction
|
||||
})]),
|
||||
target: 'map',
|
||||
view: new ol.View({
|
||||
center: [0, 0],
|
||||
zoom: 2
|
||||
})
|
||||
});
|
||||
@@ -6075,7 +6075,8 @@ olx.style.TextOptions.prototype.stroke;
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{fill: (ol.style.Fill|undefined),
|
||||
* @typedef {{geometry: (undefined|string|ol.geom.Geometry|ol.style.GeometryFunction),
|
||||
* fill: (ol.style.Fill|undefined),
|
||||
* image: (ol.style.Image|undefined),
|
||||
* stroke: (ol.style.Stroke|undefined),
|
||||
* text: (ol.style.Text|undefined),
|
||||
@@ -6085,6 +6086,15 @@ olx.style.TextOptions.prototype.stroke;
|
||||
olx.style.StyleOptions;
|
||||
|
||||
|
||||
/**
|
||||
* Feature property or geometry or function returning a geometry to render for
|
||||
* this style.
|
||||
* @type {undefined|string|ol.geom.Geometry|ol.style.GeometryFunction}
|
||||
* @api
|
||||
*/
|
||||
olx.style.StyleOptions.prototype.geometry;
|
||||
|
||||
|
||||
/**
|
||||
* Fill style.
|
||||
* @type {ol.style.Fill|undefined}
|
||||
|
||||
@@ -477,7 +477,7 @@ ol.render.canvas.Immediate.prototype.drawCircleGeometry =
|
||||
* @api
|
||||
*/
|
||||
ol.render.canvas.Immediate.prototype.drawFeature = function(feature, style) {
|
||||
var geometry = feature.getGeometry();
|
||||
var geometry = style.getGeometryFunction()(feature);
|
||||
if (!goog.isDefAndNotNull(geometry) ||
|
||||
!ol.extent.intersects(this.extent_, geometry.getExtent())) {
|
||||
return;
|
||||
|
||||
@@ -123,7 +123,7 @@ ol.renderer.vector.renderFeature = function(
|
||||
*/
|
||||
ol.renderer.vector.renderFeature_ = function(
|
||||
replayGroup, feature, style, squaredTolerance) {
|
||||
var geometry = feature.getGeometry();
|
||||
var geometry = style.getGeometryFunction()(feature);
|
||||
if (!goog.isDefAndNotNull(geometry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ ol.render.webgl.Immediate.prototype.drawCircleGeometry =
|
||||
* @api
|
||||
*/
|
||||
ol.render.webgl.Immediate.prototype.drawFeature = function(feature, style) {
|
||||
var geometry = feature.getGeometry();
|
||||
var geometry = style.getGeometryFunction()(feature);
|
||||
if (!goog.isDefAndNotNull(geometry) ||
|
||||
!ol.extent.intersects(this.extent_, geometry.getExtent())) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
goog.provide('ol.style.Style');
|
||||
goog.provide('ol.style.defaultGeometryFunction');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.functions');
|
||||
goog.require('ol.geom.Geometry');
|
||||
goog.require('ol.geom.GeometryType');
|
||||
goog.require('ol.style.Circle');
|
||||
goog.require('ol.style.Fill');
|
||||
@@ -24,6 +26,22 @@ ol.style.Style = function(opt_options) {
|
||||
|
||||
var options = goog.isDef(opt_options) ? opt_options : {};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {string|ol.geom.Geometry|ol.style.GeometryFunction}
|
||||
*/
|
||||
this.geometry_ = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {!ol.style.GeometryFunction}
|
||||
*/
|
||||
this.geometryFunction_ = ol.style.defaultGeometryFunction;
|
||||
|
||||
if (goog.isDef(options.geometry)) {
|
||||
this.setGeometry(options.geometry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {ol.style.Fill}
|
||||
@@ -57,6 +75,27 @@ ol.style.Style = function(opt_options) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {string|ol.geom.Geometry|ol.style.GeometryFunction}
|
||||
* Feature property or geometry or function that returns the geometry that will
|
||||
* be rendered with this style.
|
||||
* @api
|
||||
*/
|
||||
ol.style.Style.prototype.getGeometry = function() {
|
||||
return this.geometry_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!ol.style.GeometryFunction} Function that is called with a feature
|
||||
* and returns the geometry to render instead of the feature's geometry.
|
||||
* @api
|
||||
*/
|
||||
ol.style.Style.prototype.getGeometryFunction = function() {
|
||||
return this.geometryFunction_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {ol.style.Fill} Fill style.
|
||||
* @api
|
||||
@@ -102,6 +141,37 @@ ol.style.Style.prototype.getZIndex = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set a geometry that is rendered instead of the feature's geometry.
|
||||
*
|
||||
* @param {string|ol.geom.Geometry|ol.style.GeometryFunction} geometry
|
||||
* Feature property or geometry or function returning a geometry to render
|
||||
* for this style.
|
||||
* @api
|
||||
*/
|
||||
ol.style.Style.prototype.setGeometry = function(geometry) {
|
||||
if (goog.isFunction(geometry)) {
|
||||
this.geometryFunction_ = geometry;
|
||||
} else if (goog.isString(geometry)) {
|
||||
this.geometryFunction_ = function(feature) {
|
||||
var result = feature.get(geometry);
|
||||
if (goog.isDefAndNotNull(result)) {
|
||||
goog.asserts.assertInstanceof(result, ol.geom.Geometry);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
} else if (goog.isNull(geometry)) {
|
||||
this.geometryFunction_ = ol.style.defaultGeometryFunction;
|
||||
} else if (goog.isDef(geometry)) {
|
||||
goog.asserts.assertInstanceof(geometry, ol.geom.Geometry);
|
||||
this.geometryFunction_ = function() {
|
||||
return geometry;
|
||||
};
|
||||
}
|
||||
this.geometry_ = geometry;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the zIndex.
|
||||
*
|
||||
@@ -264,3 +334,24 @@ ol.style.createDefaultEditingStyles = function() {
|
||||
|
||||
return styles;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A function that takes an {@link ol.Feature} as argument and returns an
|
||||
* {@link ol.geom.Geometry} that will be rendered and styled for the feature.
|
||||
*
|
||||
* @typedef {function(ol.Feature): (ol.geom.Geometry|undefined)}
|
||||
* @api
|
||||
*/
|
||||
ol.style.GeometryFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Function that is called with a feature and returns its default geometry.
|
||||
* @param {ol.Feature} feature Feature to get the geometry for.
|
||||
* @return {ol.geom.Geometry|undefined} Geometry to render.
|
||||
*/
|
||||
ol.style.defaultGeometryFunction = function(feature) {
|
||||
goog.asserts.assert(!goog.isNull(feature));
|
||||
return feature.getGeometry();
|
||||
};
|
||||
|
||||
@@ -11,6 +11,53 @@ describe('ol.style.Style', function() {
|
||||
expect(style.getZIndex()).to.be(0.7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setGeometry', function() {
|
||||
var style = new ol.style.Style();
|
||||
|
||||
it('creates a geometry function from a string', function() {
|
||||
var feature = new ol.Feature();
|
||||
feature.set('myGeom', new ol.geom.Point([0, 0]));
|
||||
style.setGeometry('myGeom');
|
||||
expect(style.getGeometryFunction()(feature))
|
||||
.to.eql(feature.get('myGeom'));
|
||||
});
|
||||
|
||||
it('creates a geometry function from a geometry', function() {
|
||||
var geom = new ol.geom.Point([0, 0]);
|
||||
style.setGeometry(geom);
|
||||
expect(style.getGeometryFunction()())
|
||||
.to.eql(geom);
|
||||
});
|
||||
|
||||
it('returns the configured geometry function', function() {
|
||||
var geom = new ol.geom.Point([0, 0]);
|
||||
style.setGeometry(function() {
|
||||
return geom;
|
||||
});
|
||||
expect(style.getGeometryFunction()())
|
||||
.to.eql(geom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getGeometry', function() {
|
||||
|
||||
it('returns whatever was passed to setGeometry', function() {
|
||||
var style = new ol.style.Style();
|
||||
style.setGeometry('foo');
|
||||
expect(style.getGeometry()).to.eql('foo');
|
||||
var geom = new ol.geom.Point([1, 2]);
|
||||
style.setGeometry(geom);
|
||||
expect(style.getGeometry()).to.eql(geom);
|
||||
var fn = function() { return geom; };
|
||||
style.setGeometry(fn);
|
||||
expect(style.getGeometry()).to.eql(fn);
|
||||
style.setGeometry(null);
|
||||
expect(style.getGeometry()).to.eql(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ol.style.createStyleFunction()', function() {
|
||||
@@ -42,4 +89,6 @@ describe('ol.style.createStyleFunction()', function() {
|
||||
|
||||
});
|
||||
|
||||
goog.require('ol.Feature');
|
||||
goog.require('ol.geom.Point');
|
||||
goog.require('ol.style.Style');
|
||||
|
||||
Reference in New Issue
Block a user