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.

<!DOCTYPE html>
<html>
  <head>
    <title>Earthquake Clusters</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v5.3.0/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></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>
      import Map from 'ol/Map.js';
      import View from 'ol/View.js';
      import {createEmpty, getWidth, getHeight, extend} from 'ol/extent.js';
      import KML from 'ol/format/KML.js';
      import {defaults as defaultInteractions, Select} from 'ol/interaction.js';
      import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
      import {Cluster, Stamen, Vector as VectorSource} from 'ol/source.js';
      import {Circle as CircleStyle, Fill, RegularShape, Stroke, Style, Text} from 'ol/style.js';


      var earthquakeFill = new Fill({
        color: 'rgba(255, 153, 0, 0.8)'
      });
      var earthquakeStroke = new Stroke({
        color: 'rgba(255, 204, 0, 0.2)',
        width: 1
      });
      var textFill = new Fill({
        color: '#fff'
      });
      var textStroke = new Stroke({
        color: 'rgba(0, 0, 0, 0.6)',
        width: 3
      });
      var invisibleFill = new 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 Style({
          geometry: feature.getGeometry(),
          image: new RegularShape({
            radius1: radius,
            radius2: 3,
            points: 5,
            angle: Math.PI,
            fill: earthquakeFill,
            stroke: earthquakeStroke
          })
        });
      }

      var maxFeatureCount;
      var vector = null;
      var calculateClusterInfo = function(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 = createEmpty();
          var j = (void 0), jj = (void 0);
          for (j = 0, jj = originalFeatures.length; j < jj; ++j) {
            extend(extent, originalFeatures[j].getGeometry().getExtent());
          }
          maxFeatureCount = Math.max(maxFeatureCount, jj);
          radius = 0.25 * (getWidth(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 Style({
            image: new CircleStyle({
              radius: feature.get('radius'),
              fill: new Fill({
                color: [255, 153, 0, Math.min(0.8, 0.4 + (size / maxFeatureCount))]
              })
            }),
            text: new 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 Style({
          image: new CircleStyle({
            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 VectorLayer({
        source: new Cluster({
          distance: 40,
          source: new VectorSource({
            url: 'data/kml/2012_Earthquakes_Mag5.kml',
            format: new KML({
              extractStyles: false
            })
          })
        }),
        style: styleFunction
      });

      var raster = new TileLayer({
        source: new Stamen({
          layer: 'toner'
        })
      });

      var map = new Map({
        layers: [raster, vector],
        interactions: defaultInteractions().extend([new Select({
          condition: function(evt) {
            return evt.type == 'pointermove' ||
                evt.type == 'singleclick';
          },
          style: selectStyleFunction
        })]),
        target: 'map',
        view: new View({
          center: [0, 0],
          zoom: 2
        })
      });
    </script>
  </body>
</html>