Skip to content

Instantly share code, notes, and snippets.

@nrenner
Last active March 24, 2023 11:40
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save nrenner/5371796 to your computer and use it in GitHub Desktop.
Save nrenner/5371796 to your computer and use it in GitHub Desktop.
Inverse fill area (GeoJSON)
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="description" content="'Clipping'/highlighting an area by creating a filled outside polygon with area as hole">
<title>Inverse fill area (GeoJSON)</title>
<link rel="stylesheet" href="http://openlayers.org/api/2.12/theme/default/style.css">
<style>
html, body, #map {
margin: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="http://openlayers.org/api/2.12/OpenLayers.js"></script>
<script src="index.js"></script>
</body>
</html>
(function() {
var map;
// load admin. boundary
// GeoJSON from http://www.file-upload.net/download-7458340/62428.gjson.html
var request = OpenLayers.Request.GET({
url : "62428.geojson",
async : false
});
var format = new OpenLayers.Format.GeoJSON({
"internalProjection": "EPSG:900913",
"externalProjection": "EPSG:4326"
});
var boundary = format.read(request.responseText)[0];
var bounds = boundary.geometry.getBounds();
map = new OpenLayers.Map({
div : "map",
projection : "EPSG:900913",
displayProjection : "EPSG:4326",
controls : [],
restrictedExtent : bounds.scale(1.3)
});
map.addControl(new OpenLayers.Control.ArgParser());
map.addControl(new OpenLayers.Control.Attribution());
map.addControl(new OpenLayers.Control.LayerSwitcher());
map.addControl(new OpenLayers.Control.MousePosition());
map.addControl(new OpenLayers.Control.Navigation({
// disabled, fill rendered too slow when panning fast (base map appears)
dragPanOptions : {
enableKinetic : false
}
}));
map.addControl(new OpenLayers.Control.PanZoomBar());
map.addControl(new OpenLayers.Control.Permalink());
// base map with restricted extent and limited zoom
var zoomOffset = 11;
var osm = new OpenLayers.Layer.OSM("OSM Mapnik", null, {
zoomOffset : zoomOffset,
maxResolution : 156543.03390625 / Math.pow(2, zoomOffset),
numZoomLevels : 18 - zoomOffset + 1,
attribution : "&copy; <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
});
map.addLayer(osm);
// vector overlay for "clipping"/highlighting an area
var style = {
strokeColor : 'purple',
strokeWidth : 2,
strokeOpacity : 0.6,
fillColor : '#FFF',
fillOpacity : 1.0
};
var boundaryLayer = new OpenLayers.Layer.Vector("Boundary", {
displayInLayerSwitcher : false,
style : style,
// extend polygon clipping around map view, so that base map is always covered when panning
ratio : 3.0,
// SVG renderer has a clipping issue at high zooms (polygon disappears)
renderers : [ "VML", "Canvas" ], // "SVG",
attribution : '<br>data licensed under <a href="http://opendatacommons.org/licenses/odbl/">ODbL</a>'
});
// Create polygon with an outer outside of the viewable extent and the actual boundary as inner hole.
// disabled world-wide outer, does not work with Firefox 19 (fill disappears at zoom 16 and higher).
// var inversePolygon = map.getMaxExtent().toGeometry();
var features = [];
var inversePolygon = bounds.scale(5.0).toGeometry();
features.push(new OpenLayers.Feature.Vector(inversePolygon));
var geom = boundary.geometry;
if (geom instanceof OpenLayers.Geometry.MultiPolygon) {
// add as holes to outer polygon (admin boundary with exclaves)
for (var i = 0; i < geom.components.length; i++) {
var polygon = geom.components[i];
var linearRing = polygon.components[0];
inversePolygon.addComponent(linearRing);
}
} else if (geom instanceof OpenLayers.Geometry.Polygon) {
var linearRing = geom.components[0];
inversePolygon.addComponent(linearRing);
if (geom.components.length > 1) {
// convert inner holes to separate, standalone polygons (enclaves within admin boundary)
for (var i = 1; i < geom.components.length; i++) {
var poly = new OpenLayers.Geometry.Polygon([geom.components[i]]);
features.push(new OpenLayers.Feature.Vector(poly));
}
}
}
boundaryLayer.addFeatures(features);
map.addLayer(boundaryLayer);
if (!map.getCenter()) {
map.zoomToExtent(bounds);
}
})();
@indus
Copy link

indus commented Nov 14, 2022

@mnowaczyk is your solution done?

@mnowaczyk
Copy link

mnowaczyk commented Nov 15, 2022

@indus It's been a while, but I found that code. Here's a service that correctly reverses Multipolygon (represented as a list of polygons, which are a list of rings each). Although it doesn't check whether rings are CW or CCW, because we worked on good data from PostGIS.

import pointInPolygon from "robust-point-in-polygon";

export class GeoJsonTools
{
    public getBoundingBox(ring: any)
    {
        let xs = ring.map((a) => a[0]);
        let ys = ring.map((a) => a[1]);
        let minX = Math.min.apply(Math, xs);
        let minY = Math.min.apply(Math, ys);
        let maxX = Math.max.apply(Math, xs);
        let maxY = Math.max.apply(Math, ys);
        return {minX: minX, minY: minY, maxX: maxX, maxY: maxY};

    }

    public containsBbox(outer: any, inner: any)
    {
        let outerBox = this.getBoundingBox(outer);
        let innerBox = this.getBoundingBox(inner);
        return (
            innerBox.minX >= outerBox.minX
            && innerBox.minY >= outerBox.minY
            && innerBox.maxX <= outerBox.maxX
            && innerBox.maxY <= outerBox.maxY
        );
    }


    public contains(outer: any, inner: any)
    {
        if (!this.containsBbox(outer, inner)) {
            return false;
        }
        for (let point of inner) {
            if (!pointInPolygon(outer, point)) {
                return false;
            }
        }

        return true;

    }

    // simply check containment - they never intersect in correct data
    public findSmallestPolygon(polygons: any)
    {
        let smallest = polygons[0];
        for (let i = 1; i < polygons.length; i++) {
            if (this.containsBbox(smallest[0], polygons[i][0])) {
                smallest = polygons[i][0];
            }
        }
        return smallest;
    }


    public reverseMultipolygon(multiPolygon: any)
    {
        const reversedPolygon: any = Object.assign({}, multiPolygon);

        let outerRings = multiPolygon.coordinates.map((a)=>a[0].slice().reverse());
        let holeRings = [];
        for (let row of multiPolygon.coordinates) {
            for (let i = 1; i < row.length; i++) {
                holeRings.push(row[i].slice().reverse());
            }
        }

        let newPolygons = holeRings.map((a)=>[a]); //create nested arrays to append holes to.
        let newHoles = outerRings;


        //add whole-world polygon
        newPolygons.push([[
            [ -180, 90 ],
            [ -180, -90 ],
            [ 180, -90 ],
            [ 180, 90 ],
            [ -180, 90 ]
        ]]);


        //match holes with the smallest outer rings containing them
        for (let hole of newHoles) {
            let containingPolygons = [];
            for (let newPolygon of newPolygons) {
                if (this.contains(newPolygon[0], hole)) {
                    containingPolygons.push(newPolygon)
                }
            }
            if (containingPolygons.length) {
                let smallestContainingPolygon = this.findSmallestPolygon(containingPolygons);
                smallestContainingPolygon.push(hole)
            }
        }

        const reversedMultiPolygon: any = Object.assign({}, multiPolygon);
        reversedMultiPolygon.coordinates = newPolygons;
        return reversedMultiPolygon;
        
    }

}

@mnowaczyk
Copy link

The important part is finding in which new outer ring the new holes are, since this information is not present in the input data. This is done by first by checking the bounding box (which is vastly cheaper), and only if matched, checking the actual polygon (with a library function). Then the smallest outer ring containing the hole is selected.

@indus
Copy link

indus commented Nov 19, 2022

@mnowaczyk thanks for sharing your code 🙇 I didn't even expected an answer after all the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment