Earthquake Clusters

Demonstrates the use of style geometries to render source features of a cluster.

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.

To achieve this, we make heavy use of style functions and ol.style.Style#geometry.

<!DOCTYPE html>
<html>
  <head>
    <title>Earthquake Clusters</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v4.6.4/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src=""></script>
    <script src="https://openlayers.org/en/v4.6.4/build/ol.js"></script>
    <style>
      #map {
        position: relative;
      }
      #info {
        position: absolute;
        height: 1px;
        width: 1px;
        z-index: 100;
      }
      .tooltip.in {
        opacity: 1;
      }
      .tooltip.top .tooltip-arrow {
        border-top-color: white;
      }
      .tooltip-inner {
        border: 2px solid white;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <script>
      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, vector;
      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();
          var j, jj;
          for (j = 0, jj = originalFeatures.length; j < jj; ++j) {
            ol.extent.extend(extent, originalFeatures[j].getGeometry().getExtent());
          }
          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) {
        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;
      }

      vector = new ol.layer.Vector({
        source: new ol.source.Cluster({
          distance: 40,
          source: new ol.source.Vector({
            url: 'https://openlayers.org/en/v4.6.4/examples/data/kml/2012_Earthquakes_Mag5.kml',
            format: new ol.format.KML({
              extractStyles: false
            })
          })
        }),
        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.type == 'pointermove' ||
                evt.type == 'singleclick';
          },
          style: selectStyleFunction
        })]),
        target: 'map',
        view: new ol.View({
          center: [0, 0],
          zoom: 2
        })
      });
    </script>
  </body>
</html>