Skip to content

Instantly share code, notes, and snippets.

@bseddon
Created January 26, 2023 23:47
Show Gist options
  • Save bseddon/6020ca765ee2ae4f5d3cb77279e49383 to your computer and use it in GitHub Desktop.
Save bseddon/6020ca765ee2ae4f5d3cb77279e49383 to your computer and use it in GitHub Desktop.

In the previous gist it was shown how to retrieve geometries in order to create SVG maps using D3. A geometry is just a sequence of latitude and longitude values for a shape. The same geomtries can be used to create overlays for Google Maps. Creating an overlay using a single array of points is covered in the Google Maps API documentation. Instead this gist covers handing multiple geomtries where the aim is to merge them so they can be shown as one continuous area.

The scenario might that you have geometries for all the counties of a state an want to use them to show the whole state. In this example, the aimn is to show the area served by the NJ Cumberland, Salem and Cape May Workforce Board. Given the name, it will no surprise that the board covers these three counties. There is no single geometry for this area but geometries for the three counties are available from the US Census Bureau.

Overlaying each county on Google Maps is one way to do this but then the county boundaries will show. So in this example the geometries for the three counties will be merged and displayed as one overlay.

The credit for this approach needs to go to geocodezip for answer 1 in this Stack Overflow post. The enhancement her is to generalize the technique shown in the post and to apply it in a real example.

The geometries for the three NJ counties are no reproduced here. They can be found in the counties file on the cartographic boundaries page of the US Census Bureau.

map.html

<html>
  <head>
    <title>Cumberland County</title>
    <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
    <script src="https://cdn.jsdelivr.net/gh/bjornharrtell/jsts@gh-pages/1.0.2/jsts.min.js"></script>

    <link rel="stylesheet" type="text/css" href="./style.css" />
    <script type="module" src="./index.js"></script>
  </head>
  <body>
    <div id="map"></div>

    <!-- 
      The `defer` attribute causes the callback to execute after the full HTML
      document has been parsed. For non-blocking uses, avoiding race conditions,
      and consistent behavior across browsers, consider loading using Promises
      with https://www.npmjs.com/package/@googlemaps/js-api-loader.
      -->
    <script
      src="https://maps.googleapis.com/maps/api/js?key=<You Google Maps key>&callback=initMap&v=weekly"
      defer
    ></script>
  </body>
</html>

style.css

/* 
 * Always set the map height explicitly to define the size of the div element
 * that contains the map. 
 */
#map {
    height: 100%;
}

/* 
* Optional: Makes the sample page fill the window. 
*/
html,
body {
    height: 100%;
    margin: 0;
    padding: 0;
}

index.js

/**
 * Converts an array of coordinates in Google Maps format into JSTS format
 * @param {Object[]} boundaries 
 * @returns jsts.geom.Coordinate[]
 */
function googleMaps2JSTS (boundaries) 
{
    var coordinates = [];
    for (var i = 0; i < boundaries.getLength(); i++)
    {
        coordinates.push(new jsts.geom.Coordinate(
            boundaries.getAt(i).lat(), boundaries.getAt(i).lng()
        ));
    }
    return coordinates;
};

/**
 * 
 * @param {*} geometry 
 * @returns 
 */
function jsts2googleMapsPath(geometry) 
{
    var coordArray = geometry.getCoordinates();
    var GMcoords = [];
    for (var i = 0; i < coordArray.length; i++) 
    {
        GMcoords.push(new google.maps.LatLng(coordArray[i].x, coordArray[i].y));
    }
    return GMcoords;
}    

/**
 * Create a JSTS polygon by first creating a GoogleMaps polygon and converting it
 * @param {Object[]} coords Each element is an object with lat and lng properties
 * @returns Object
 */
var createJSTSPolygon = function( coords )
{
    var geometryFactory = new jsts.geom.GeometryFactory();

    // Construct the polygon.
    const polygon = new google.maps.Polygon({
        paths: coords,
        strokeColor: "#FF0000",
        strokeOpacity: 0.5,
        strokeWeight: 1,
        fillColor: "#FF0000",
        fillOpacity: 0.1
    });

    var polygonJSTS = geometryFactory.createPolygon(geometryFactory.createLinearRing(googleMaps2JSTS(polygon.getPath())));
    polygonJSTS.normalize();

    return polygonJSTS;
}

/**
 * Create a JSTS polygon that merges the paths implied by any number of coordinate geometry lists
 * @param {Object[][]} coords Each element is an array of object each with lat and lng properties
 * @returns 
 */
function createMergedPolygon( coords )
{
    let union = null;
    coords.forEach( coord => 
    {
        let jstsPolygon = createJSTSPolygon( coord );
        union = union ? union.union( jstsPolygon ) : jstsPolygon;
    } )
    return union;
}

// This example creates a simple polygon representing the Bermuda Triangle.
function initMap() 
{
    // 
    google.maps.Polygon.prototype.getBounds = function()
    {
        let bounds = new google.maps.LatLngBounds();
        this.getPath().forEach(function(element,index){bounds.extend(element)})
        return bounds
    }

    // Define the LatLng coordinates for the polygon's path.
    let cumberlandCoords = 
    [
		{"lng":-75.419561999999999,"lat":39.413254999999999},
		...
    ];

    let salemCoords = [
        {"lat": 39.655113,"lng": -75.526743999999994},
		...
    ]

    let capemayCoords = [
        {"lat": 38.940582017256801,"lng": -74.971908603890697},
		...
    ]

    let mergedPolygon = createMergedPolygon( [cumberlandCoords, salem2Coords, capemayCoords] );

    var outputPath = jsts2googleMapsPath(mergedPolygon);

    // Construct the polygon.
    const merged = new google.maps.Polygon({
        paths: outputPath,
        strokeColor: "#FF0000",
        strokeOpacity: 0.5,
        strokeWeight: 1,
        fillColor: "#FF0000",
        fillOpacity: 0.1,
    });

    // The bound is used to make use Google Maps displays all the described area and to get the center
    let bounds = merged.getBounds();
    let center = bounds.getCenter();

    const map = new google.maps.Map(document.getElementById("map"), 
    {
        zoom: 12,
        center: {"lng": center.lng(),"lat": center.lat() },
        mapTypeId: "terrain",
    });

    // Make sure the polygon can be shown
    map.fitBounds(bounds);

    merged.setMap(map);
}

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