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);
}
})();
@mnowaczyk
Copy link

FYI, this won't work if the other polygons are inside holes in the first polygon, e.g Netherlands/Baarle-Nassau.

@nrenner
Copy link
Author

nrenner commented Dec 9, 2019

Thanks for the hint.

For Leaflet, you might want to give these plugins a try:
https://github.com/ebrelsford/Leaflet.snogylop
https://github.com/aparshin/leaflet-boundary-canvas

edit: plugins are for Leaflet (Gist is OpenLayers 2)

@mnowaczyk
Copy link

mnowaczyk commented Dec 10, 2019

I didn't test it (I'm not using Leaflet), but after reading the code I'm sure it won't work either. The plugin just takes the outer edge of every polygon and turns it into a hole in the world rectangle, while holes in the original polygons are discarded. This will crash when one of these outer polygons is inside a hole, because intersecting holes do not make a valid polygon.
The problem is that all outer bounaries are on the top level of this data structure, even if geometrically they're inside holes, while holes must be nested inside a specific outer boundary. The best idea I have come up with is to check if every new hole (original outer boundary) is contained in every new outer boundary (original hole) and if it is, treat it as a hole in this particular polygon, instead of a hole in the world rectangle, but this seems a bit CPU-heavy. I'll post the solution when it's done.

@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