Hands On

How to Find Out Whether a Location is on Land or Water

By Richard Süselbeck | 27 October 2020

Try HERE Maps

Create a free API key to build location-aware apps and services.

I was recently working on a different project and thought it might be useful to know whether a given location (latitude and longitude) is either on land or in water. Finding out how to do this was a little bit tricky, so I thought I’d share how to do this in a blog post. What we’ll be building is a simple web app, which allows you to click on a map. When you do this, an info bubble will pop up to tell you whether you clicked on land or water! If you’re in water, the info bubble will also tell you what the name of the body of water is.

Before we get started, you can check out the final project on land-or-water.com. (Yes, I bought a domain for a side project. Sigh. I blame my colleague Ray who “encouraged” me.) You can also check out the full code for the project on GitHub. With that out of the way, let’s get started!

Step 1: Introducing the Advanced Data Sets API

One of our more hidden and also more powerful APIs is the Advanced Data Sets API. This allows you to access a lot of our detailed map data directly, without going through any of our other APIs. Essentially the API offers map attributes as key/value pairs, which are grouped in layers. This is a big API with a lot of endpoints, so I will only cover the parts that are relevant for this use case. Please read the documentation for the full details.

Step 2: Searching for the Right Layers

Now my first thought was that there might a layer that contains information on water bodies. To find out whether that was the case, I used the layers endpoint of the API to get a list of all the layers currently available in the Advanced Data Sets API.


https://s.fleet.ls.hereapi.com/1/doc/layers.json?apikey={apiKey}

As you can see this API call doesn’t need any parameters, except for an API Key. If you don’t have one yet, just sign up for your own Freemium account. It only takes a minute.

This call returns a long(!) list of layers. Here’s the very first one in the list:


[
{
"type": "geom",
"tileLevel": 9,
"attributes": [
"HPX",
"HPY",
"HPZ",
"SLOPES",
"CURVATURES",
"VERTICAL_FLAGS",
],
},


You can see the layer name (ADAS_ATTRIB_FC1) and the list of attributes in the layer. Please note the featureMapping value. This indicates whether the specific layer is part of your Freemium plan. Since this layer is “Premium” it is not, but the layers marked as “Base” are.

ADAS stands for Advanced Driver Assistance Systems, and this layer contains high definition map data for autonomous driving. That’s pretty cool, actually, but not what I was looking for. So, I went for a scroll in the list and looked for layer names that seemed helpful. The most obvious candidates were called CARTO_POLY_OCEAN, CARTO_POLY_RIVER_DOx (with x being 1,2,3 and 4).

However, just because the name sounds right doesn’t mean it is right. Also, rivers and oceans aren’t the only bodies of water in the world. What about lakes and bays and harbors and so on? I needed to dig deeper and started to use the layer endpoint (note the lack of a trailing s) to get more details about other layers that seemed interesting. This endpoint provides a description of a given layer. Here’s how to check what the layer CARTO_POLY_DO2 contains.


https://s.fleet.ls.hereapi.com/1/doc/layer.json
?apikey={{apiKey}}
&layer=CARTO_POLY_RIVER_DO2


You’ll note that the response contains a textual description as well as a list of FEATURE_TYPE. One of these is “Lake” and another is “Bay/Harbour”, which is exactly what we are looking for!


{
"description": "Cartographic area polygons, except for administrative areas.Polygons are cut at tile boundaries, so that a polygon is decomposed into smaller pieces.Coordinate values with a leading zero indicate that the line to the next coordinate is artificially introduced by tiling or removal of inner rings.",
"attributes": {
"CARTO_ID": "Permanent carto ID.",
"FACE_ID": "Permanent face ID. A carto can consist of multiple faces. Each polygon is a \"face\". Hence, one CARTO_ID can comprise multiple faces.",
"FEATURE_TYPE": "Feature type that describes the carto type:500412 : River500413 : Intermittent River500414 : Canal/Water Channel",
"NAMES": "The names of this carto. Cartos can have multiple names, in the same or in multiple languages, transliterations of these names and phonemes. This field contains all of them.",
"LAT": "Latitude coordinates [10^-5 degree WGS84] along the carto polygon. Comma separated. Clockwise orientation. Each value is relative to the previous.",
"LON": "Longitude coordinates [10^-5 degree WGS84] along the carto polygon. Comma separated. Clockwise orientation. Each value is relative to the previous.",
"INNER_LAT": "Latitude coordinates [10^-5 degree WGS84] of the inner rings of LAT/LON polygon. Coordinates are comma separated. Each value is relative to the previous. Inner rings are semicolon seperated",
"INNER_LON": "Longitude coordinates [10^-5 degree WGS84] of the inner rings of LAT/LON polygon. Coordinates are comma separated. Each value is relative to the previous. Inner rings are semicolon seperated"
},
"referencedStaticContents": [],
"tileRequestsLevel": 11,
"tileX": 1870,
"tileY": 1339,
"isStaticContent": false
}


Using my detective skills and the layer endpoint, I was able to build a list of all layers and all feature types inside these layers that are relevant to our question.


let layers =
"CARTO_POLY_OCEAN," +
"CARTO_POLY_RIVER_DO1," +
"CARTO_POLY_RIVER_DO2," +
"CARTO_POLY_RIVER_DO3," +
"CARTO_POLY_RIVER_DO4," +
"CARTO_POLY_DO2," +
"CARTO_POLY_DO3," +
"CARTO_POLY_DO4";

let features = [
"500116", //Ocean
"500421", //Lake
"500412", //River
"507116", //Bay/Harbour
"9997008", //Seaport/Harbour
];


Step 3: Checking Proximity

To answer our question (land or water?) we now need to find out whether a given location is inside one of these features. That’s where the proximity endpoint comes into play. This endpoint returns all content in a given map layer or layers that are within a radius of a given location.

Here’s how we can check whether we are in the ocean:


https://fleet.ls.hereapi.com/1/search/proximity.json
?layer_ids=CARTO_POLY_OCEAN
&apiKey={{apiKey}}
&proximity=41.771312,-26.542969
&key_attributes=NAMES


Note that we are providing a proximity location as latitude and longitude. This parameter also supports a radius, but only want to know about our specific location, so we are omitting this for an effective radius of 0. We also provide a list of layers, in this case only CARTO_POLY_OCEAN. Finally, we need to provide a key attribute from the layer, which we can get using the layer endpoint. In this case we have chosen NAMES. The response looks like this.

We can see that the returned attributes contain the names of the ocean feature we are in, as well as a feature type. So, if the response contains any geometries with a feature type from our list from Step 2, we are in water.

Let’s quickly expand this API call to include all relevant layers. Note that we need to provide a key attribute for every single layer.


https://fleet.ls.hereapi.com/1/search/proximity.json
?layer_ids=CARTO_POLY_OCEAN,CARTO_POLY_RIVER_DO1,CARTO_POLY_RIVER_DO2,CARTO_POLY_RIVER_DO3,CARTO_POLY_RIVER_DO4,CARTO_POLY_DO2,CARTO_POLY_DO3,CARTO_POLY_DO4
&apiKey={{apiKey}}
&proximity=41.771312,-26.542969
&key_attributes=NAMES,NAMES,NAMES,NAMES,NAMES,NAMES,NAMES,NAMES


Step 4: Building the web app

Now we are ready to build the web app. Since this isn’t really a tutorial on how to use the JavaScript API, I will mostly refer you to the finished GitHub project and the documentation for the JavaScript API. Some quick notes, however.

This project uses Fetch to make the exact API call from above. It then checks whether the geometries are empty. If so, we are on land. If they are not empty, it makes sure that feature type returned is one from the list we built above. If so, we are on water. Not so tricky in the end.


let feature;
if (response.geometries.length > 0) {
feature = response.geometries[0].attributes.FEATURE_TYPE;

if (features.includes(feature)) {
isWater = true;
let featureNames = response.geometries[0].attributes.NAMES;
featureName = featureNames.substring(5, featureNames.length);
let separator = '\u001E';
let index = featureName.indexOf(separator);

if (index != -1) {
featureName = featureName.substring(0, index);
}

} else {
isWater = false;
}

} else {
isWater = false;
}


Finally, it grabs the first name from the list of names from the response and drops an InfoBubble on the map.


let html;

if (isWater) {
html = '<div align="center">🌊 Water! 🌊</div><div align="center" style="width:250px">In fact, this is the <em>${FEATURE}!</em></div>'; html = html.replace('${FEATURE}', featureName);
} else {
html = '<div align="center">🌲 Land! 🌲';
}

if (lastBubble != null) {
lastBubble.close();
}

let bubble = new H.ui.InfoBubble({ lat: lat, lng: lng }, { content: html });
lastBubble = bubble;