3

How do you split an OpenLayers Vector Feature (Polygon or a LineString)?

i.e

ux: select feature, select a 'split' UI button, draw a split on-top of the polygon or through the line. The feature would then be split into multiple features that I have access too.

Are there any Split functions/interactions from either OpenLayers or a 3rd party library that you can use or how would you implement this manually?

0

Solution below is based upon port and improvement of similar solution for Leaflet (see Splitting A polygon into multiple polygon by multiple line strings in Leaflet and turf.js).

Cutting of polygon with line is done with the help of Turf.js library. Turf.js library does not have explicit method to split polygon with line. The most convenient method for this purpose is then turf.difference(poly1, poly2), which cuts out second polygon from first. If second polygon is very thin and long rectangle (line with small 'height'), this can be used as a split method.

This is done in two steps. First step is to 'fatten' dividing line to one side, cut polygon by it and take into account split polygon(s) one the opposite side of the line. Then dividing line is 'fattened' to the other side, polygon is cut by it and split polygon(s) on opposite side is taken into account.

This way polygon of any shape can be cut with line of any shape.

Result of cut is feature collection of cut polygons, where each polygon has feature id in the form idPrefixN.M, where idPrefix is input parameter to cut function, N is number of cut side (1 or 2) and M is sequential number of polygon on relevant side.

Code of cut function:

function polygonCut(polygon, line, idPrefix) {
  const THICK_LINE_UNITS = 'kilometers';
  const THICK_LINE_WIDTH = 0.001;
  var i, j, id, intersectPoints, lineCoords, forCut, forSelect;
  var thickLineString, thickLinePolygon, clipped, polyg, intersect;
  var polyCoords = [];
  var cutPolyGeoms = [];
  var cutFeatures = [];
  var offsetLine = [];
  var retVal = null;

  if (((polygon.type != 'Polygon') && (polygon.type != 'MultiPolygon')) || (line.type != 'LineString')) {
    return retVal;
  }

  if (typeof(idPrefix) === 'undefined') {
    idPrefix = '';
  }

  intersectPoints = turf.lineIntersect(polygon, line);
  if (intersectPoints.features.length == 0) {
    return retVal;
  }

  var lineCoords = turf.getCoords(line);
  if ((turf.booleanWithin(turf.point(lineCoords[0]), polygon) ||
      (turf.booleanWithin(turf.point(lineCoords[lineCoords.length - 1]), polygon)))) {
    return retVal;
  }

  offsetLine[0] = turf.lineOffset(line, THICK_LINE_WIDTH, {units: THICK_LINE_UNITS});
  offsetLine[1] = turf.lineOffset(line, -THICK_LINE_WIDTH, {units: THICK_LINE_UNITS});

  for (i = 0; i <= 1; i++) {
    forCut = i; 
    forSelect = (i + 1) % 2; 
    polyCoords = [];
    for (j = 0; j < line.coordinates.length; j++) {
      polyCoords.push(line.coordinates[j]);
    }
     for (j = (offsetLine[forCut].geometry.coordinates.length - 1); j >= 0; j--) {
      polyCoords.push(offsetLine[forCut].geometry.coordinates[j]);
    }
    polyCoords.push(line.coordinates[0]);

    thickLineString = turf.lineString(polyCoords);
    thickLinePolygon = turf.lineToPolygon(thickLineString);
    clipped = turf.difference(polygon, thickLinePolygon);

    cutPolyGeoms = [];
    for (j = 0; j < clipped.geometry.coordinates.length; j++) {
      polyg = turf.polygon(clipped.geometry.coordinates[j]);
      intersect = turf.lineIntersect(polyg, offsetLine[forSelect]);
      if (intersect.features.length > 0) {
        cutPolyGeoms.push(polyg.geometry.coordinates);
      };
    };

    cutPolyGeoms.forEach(function (geometry, index) {
      id = idPrefix + (i + 1) + '.' +  (index + 1);
      cutFeatures.push(turf.polygon(geometry, {id: id}));
    });
  }

  if (cutFeatures.length > 0) retVal = turf.featureCollection(cutFeatures);

  return retVal;
};

Example of usage of this function is available at JSFiddle: https://jsfiddle.net/TomazicM/157s3Lmt/. Example allows splitting of polygons multiple times with lines of any shape.

At each step (split) the following layers and arrays are updated:

  • Layer drawnPolygons contains all polygons, split and unsplit
  • Layer drawnLines contains all lines used for splitting
  • Array polygons contains all polygons that correspond to drawnPolygons layer

The main part of the code:

  var raster = new ol.layer.Tile({
    source: new ol.source.OSM()
  });

  var sourceDrawnPolygons = new ol.source.Vector({wrapX: false});
  var drawnPolygons = new ol.layer.Vector({
    source: sourceDrawnPolygons,
    style: drawnStyle
  });
  var sourceDrawnLines = new ol.source.Vector({wrapX: false});
  var drawnLines = new ol.layer.Vector({
    source: sourceDrawnLines
  });

  var map = new ol.Map({
    layers: [raster, drawnPolygons, drawnLines],
    target: 'map',
    view: new ol.View({
      center: [-11000000, 4600000],
      zoom: 14
    })
  });

  var pressedButton;       

  function addInteraction(button) {
    var value = button.value;
    pressedButton = button;
    button.style.backgroundColor = '#A8D3EE';
    if (draw != null) {
      map.removeInteraction(draw);
    }
    draw = new ol.interaction.Draw({
      source: (value == 'Polygon') ? sourceDrawnPolygons : sourceDrawnLines,
      type: value
    });
    map.addInteraction(draw);
    draw.on('drawend', drawEnd);     
  }

  const cutIdPrefix = 'cut_';
  var draw = null;
  var FormatGeoJSON = new ol.format.GeoJSON; 
  var polygons = [];

  var defaultStyle = new ol.layer.Vector().getStyle()();

  var side1CutStyle = new ol.style.Style({
    stroke: new ol.style.Stroke({
      color: [0, 255, 0, 0.8],
      width: 1
    }),
    fill: new ol.style.Fill({
      color: [0, 255, 0, 0.2],
    })
  });

  var side2CutStyle = new ol.style.Style({
    stroke: new ol.style.Stroke({
      color: [255, 0, 0, 0.8],
      width: 1
    }),
    fill: new ol.style.Fill({
      color: [255, 0, 0, 0.2],
    })
  });

  function drawnStyle(feature) {
    var id = feature.get('id');
    if (typeof(id) !== 'undefined') {
      id = id.substring(0, (cutIdPrefix.length + 1))
    }
    if (id == cutIdPrefix + '1')
      return side1CutStyle;
    else if (id == cutIdPrefix + '2')
      return side2CutStyle;
    else {
      return defaultStyle;
    }
  }

  function drawEnd(e) {
    var drawnGeoJSON = FormatGeoJSON.writeFeatureObject(e.feature, {dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857'});
    var drawnGeometry = turf.getGeom(drawnGeoJSON);

    if (drawnGeometry.type == 'Polygon') {
      var unkinked = turf.unkinkPolygon(drawnGeometry);
      turf.geomEach(unkinked, function (geometry) {
        polygons.push(geometry);
      });
      }
    else if (drawnGeometry.type == 'LineString') {
      var newPolygons = [];
      polygons.forEach(function (polygon, index) {
        var cutPolygon = polygonCut(polygon, drawnGeometry, cutIdPrefix);
        if (cutPolygon != null) {
          feature = FormatGeoJSON.readFeatures(cutPolygon, {dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857'});
          sourceDrawnPolygons.addFeatures(feature);
          turf.geomEach(cutPolygon, function (geometry) {
            newPolygons.push(geometry);
          });
          }
        else {
          newPolygons.push(polygon);
        }
      });
      polygons = newPolygons;
    }

    map.removeInteraction(draw);
    pressedButton.style.backgroundColor = '';
    pressedButton.blur();
  }

Here is an example of complex polygon cut:

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.