diff --git a/examples/measure-style.html b/examples/measure-style.html
new file mode 100644
index 0000000000..0baea158e7
--- /dev/null
+++ b/examples/measure-style.html
@@ -0,0 +1,33 @@
+---
+layout: example.html
+title: Measure using vector styles
+shortdesc: Example of measuring lengths and areas using vector styles.
+docs: >
+
Using vector styles instead of overlays makes it easy to set up, then modify and
+ clear a measure. Additional information such as the lengths of individual segments
+ or sides can be included as required.
+ The getLength() and getArea()
+ functions calculate spherical lengths and areas for geometries. Lengths are
+ calculated by assuming great circle segments between geometry coordinates.
+ Areas are calculated as if edges of polygons were great circle segments.
+ Note that the geometry.getLength() and geometry.getArea()
+ methods return measures of projected (planar) geometries. These can be very
+ different than on-the-ground measures in certain situations — in northern
+ and southern latitudes using Web Mercator for example. For better results,
+ use the functions in the ol/sphere module.
+tags: "draw, edit, measure, modify, style, vector"
+---
+
+
diff --git a/examples/measure-style.js b/examples/measure-style.js
new file mode 100644
index 0000000000..3b7e6dbe34
--- /dev/null
+++ b/examples/measure-style.js
@@ -0,0 +1,274 @@
+import Map from '../src/ol/Map.js';
+import View from '../src/ol/View.js';
+import {
+ Circle as CircleStyle,
+ Fill,
+ RegularShape,
+ Stroke,
+ Style,
+ Text,
+} from '../src/ol/style.js';
+import {Draw, Modify} from '../src/ol/interaction.js';
+import {LineString, Point} from '../src/ol/geom.js';
+import {OSM, Vector as VectorSource} from '../src/ol/source.js';
+import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
+import {getArea, getLength} from '../src/ol/sphere.js';
+
+const typeSelect = document.getElementById('type');
+const showSegments = document.getElementById('segments');
+const clearPrevious = document.getElementById('clear');
+
+const style = new Style({
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 0.2)',
+ }),
+ stroke: new Stroke({
+ color: 'rgba(0, 0, 0, 0.5)',
+ lineDash: [10, 10],
+ width: 2,
+ }),
+ image: new CircleStyle({
+ radius: 5,
+ stroke: new Stroke({
+ color: 'rgba(0, 0, 0, 0.7)',
+ }),
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 0.2)',
+ }),
+ }),
+});
+
+const labelStyle = new Style({
+ text: new Text({
+ font: '14px Calibri,sans-serif',
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 1)',
+ }),
+ backgroundFill: new Fill({
+ color: 'rgba(0, 0, 0, 0.7)',
+ }),
+ padding: [3, 3, 3, 3],
+ textBaseline: 'bottom',
+ offsetY: -15,
+ }),
+ image: new RegularShape({
+ radius: 8,
+ points: 3,
+ angle: Math.PI,
+ displacement: [0, 10],
+ fill: new Fill({
+ color: 'rgba(0, 0, 0, 0.7)',
+ }),
+ }),
+});
+
+const tipStyle = new Style({
+ text: new Text({
+ font: '12px Calibri,sans-serif',
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 1)',
+ }),
+ backgroundFill: new Fill({
+ color: 'rgba(0, 0, 0, 0.4)',
+ }),
+ padding: [2, 2, 2, 2],
+ textAlign: 'left',
+ offsetX: 15,
+ }),
+});
+
+const modifyStyle = new Style({
+ image: new CircleStyle({
+ radius: 5,
+ stroke: new Stroke({
+ color: 'rgba(0, 0, 0, 0.7)',
+ }),
+ fill: new Fill({
+ color: 'rgba(0, 0, 0, 0.4)',
+ }),
+ }),
+ text: new Text({
+ text: 'Drag to modify',
+ font: '12px Calibri,sans-serif',
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 1)',
+ }),
+ backgroundFill: new Fill({
+ color: 'rgba(0, 0, 0, 0.7)',
+ }),
+ padding: [2, 2, 2, 2],
+ textAlign: 'left',
+ offsetX: 15,
+ }),
+});
+
+const segmentStyle = new Style({
+ text: new Text({
+ font: '12px Calibri,sans-serif',
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 1)',
+ }),
+ backgroundFill: new Fill({
+ color: 'rgba(0, 0, 0, 0.4)',
+ }),
+ padding: [2, 2, 2, 2],
+ textBaseline: 'bottom',
+ offsetY: -12,
+ }),
+ image: new RegularShape({
+ radius: 6,
+ points: 3,
+ angle: Math.PI,
+ displacement: [0, 8],
+ fill: new Fill({
+ color: 'rgba(0, 0, 0, 0.4)',
+ }),
+ }),
+});
+
+const segmentStyles = [segmentStyle];
+
+const formatLength = function (line) {
+ const length = getLength(line);
+ let output;
+ if (length > 100) {
+ output = Math.round((length / 1000) * 100) / 100 + ' km';
+ } else {
+ output = Math.round(length * 100) / 100 + ' m';
+ }
+ return output;
+};
+
+const formatArea = function (polygon) {
+ const area = getArea(polygon);
+ let output;
+ if (area > 10000) {
+ output = Math.round((area / 1000000) * 100) / 100 + ' km\xB2';
+ } else {
+ output = Math.round(area * 100) / 100 + ' m\xB2';
+ }
+ return output;
+};
+
+const raster = new TileLayer({
+ source: new OSM(),
+});
+
+const source = new VectorSource();
+
+const modify = new Modify({source: source, style: modifyStyle});
+
+let tipPoint;
+
+function styleFunction(feature, segments, drawType, tip) {
+ const styles = [style];
+ const geometry = feature.getGeometry();
+ const type = geometry.getType();
+ let point, label, line;
+ if (!drawType || drawType === type) {
+ if (type === 'Polygon') {
+ point = geometry.getInteriorPoint();
+ label = formatArea(geometry);
+ line = new LineString(geometry.getCoordinates()[0]);
+ } else if (type === 'LineString') {
+ point = new Point(geometry.getLastCoordinate());
+ label = formatLength(geometry);
+ line = geometry;
+ }
+ }
+ if (segments && line) {
+ let count = 0;
+ line.forEachSegment(function (a, b) {
+ const segment = new LineString([a, b]);
+ const label = formatLength(segment);
+ if (segmentStyles.length - 1 < count) {
+ segmentStyles.push(segmentStyle.clone());
+ }
+ const segmentPoint = new Point(segment.getCoordinateAt(0.5));
+ segmentStyles[count].setGeometry(segmentPoint);
+ segmentStyles[count].getText().setText(label);
+ styles.push(segmentStyles[count]);
+ count++;
+ });
+ }
+ if (label) {
+ labelStyle.setGeometry(point);
+ labelStyle.getText().setText(label);
+ styles.push(labelStyle);
+ }
+ if (
+ tip &&
+ type === 'Point' &&
+ !modify.getOverlay().getSource().getFeatures().length
+ ) {
+ tipPoint = geometry;
+ tipStyle.getText().setText(tip);
+ styles.push(tipStyle);
+ }
+ return styles;
+}
+
+const vector = new VectorLayer({
+ source: source,
+ style: function (feature) {
+ return styleFunction(feature, showSegments.checked);
+ },
+});
+
+const map = new Map({
+ layers: [raster, vector],
+ target: 'map',
+ view: new View({
+ center: [-11000000, 4600000],
+ zoom: 15,
+ }),
+});
+
+map.addInteraction(modify);
+
+let draw; // global so we can remove it later
+
+function addInteraction() {
+ const drawType = typeSelect.value;
+ const activeTip =
+ 'Click to continue drawing the ' +
+ (drawType === 'Polygon' ? 'polygon' : 'line');
+ const idleTip = 'Click to start measuring';
+ let tip = idleTip;
+ draw = new Draw({
+ source: source,
+ type: drawType,
+ style: function (feature) {
+ return styleFunction(feature, showSegments.checked, drawType, tip);
+ },
+ });
+ draw.on('drawstart', function () {
+ if (clearPrevious.checked) {
+ source.clear();
+ }
+ modify.setActive(false);
+ tip = activeTip;
+ });
+ draw.on('drawend', function () {
+ modifyStyle.setGeometry(tipPoint);
+ modify.setActive(true);
+ map.once('pointermove', function () {
+ modifyStyle.setGeometry();
+ });
+ tip = idleTip;
+ });
+ modify.setActive(true);
+ map.addInteraction(draw);
+}
+
+typeSelect.onchange = function () {
+ map.removeInteraction(draw);
+ addInteraction();
+};
+
+addInteraction();
+
+showSegments.onchange = function () {
+ vector.changed();
+ draw.getOverlay().changed();
+};