Create a tapered line with OpenLayers and GeoJSON - javascript

I am trying to create an offline map through the following steps:
Download data from Natural Earth Data
Convert the shape files to GeoJSON
Add the GeoJSON files as layers in OpenLayers3
I am struggling to get the rivers right, I can display them, but only as a line with a fixed width. However, when looking at the file I created from Natural Earth Data I see that there are in fact many short lines, and each has a width (strokeweig) specified. See snippet below for illustration.
{
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": { "strokeweig": 0.20000000300000001, "scalerank": 5, "featurecla": "River", "name": null, "dissolve": "River_untitled_77", "note": "_untitled_77" }, "geometry": { "type": "LineString", "coordinates": [ [ -72.991327277300684, 46.177440090512803 ], [ -73.078557095009359, 46.160128485695026 ], [ -73.146304897744017, 46.123541571632373 ], [ -73.177181566038399, 46.070624904965499 ], [ -73.163952399371681, 46.044166571632061 ] ] } },
{ "type": "Feature", "properties": { "strokeweig": 0.149999991, "scalerank": 5, "featurecla": "River", "name": "Ebro", "dissolve": "RiverEbro", "note": null }, "geometry": { "type": "LineString", "coordinates": [ [ -4.188860236009816, 43.011173407557422 ], [ -4.10225053548865, 43.001484076502706 ], [ -4.054759894212424, 42.952520656906145 ], [ -4.017449510097691, 42.861053371749534 ], [ -3.96267249186829, 42.825034898442098 ], [ -3.890377163091955, 42.844413560551544 ], [ -3.821957566737524, 42.841855577153098 ], [ -3.757387864588821, 42.81728343359832 ], [ -3.70925126790894, 42.832631333988999 ], [ -3.677521938481732, 42.887899278325165 ], [ -3.626775681971111, 42.89800202083822 ], [ -3.52213090658006, 42.845447089197393 ] ] } },
....
]
So, my question is twofold:
How do I work with this types of data in the first place to display a line with a width as specified in the strokeweig property of a features array item?
How do I deal with this value when zooming in and out?
Thanks,
Hendrik

You can find an example here http://openlayersbook.github.io/ch06-styling-vector-layers/example-07.html that is very near to your needs.
There is not a built-in solution. I can imagine at least two ways: Instead of creating one layer for the whole rivers featurecollection, create one layer for each "scalerank", and set their min/maxResolution accordingly. Otherwise, you can listen to the MapView change:resolution event, and add/remove or show/hide the features accordingly.

The code snippet below does sort of what I wanted:
var layer = new ol.layer.Vector({
source: new ol.source.Vector({
url: 'maps/rivers.json',
format: new ol.format.GeoJSON()
}),
style: function(feature, resolution) {
var strokeWeight, scaleRank, width, style;
strokeWeight = feature.get('strokeweig');
scaleRank = feature.get('scalerank');
// strokeWeight is multiplied by scaleRanks because I think this is why this property was included in the GeoJSON
// This number is then corrected for different zoom levels, max-resolution~611.5 based on map.MaxZoom=18
// The calculation with dividers represents the amount of times the current resolution is SMALLER than
// the max-resolution. Note that the value 611.5 will need to be changed when we decide the
// map.maxZoom needs to be increased
width = strokeWeight * scaleRank * (611.5 / resolution));
style = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'rgb(35, 51, 60)',
width: width
})
});
return [style];
}
});
I am not super happy with the formula that computes the width. Obviously taking a static value is not ideal and I don't know how the properties actually should have been combined to produce the correct line width.
I think I will be looking into the solution that Francesco suggested to improve it. This still won't be ideal because the max scaleRank in the file is 6 and I will be allowing zoom up to 18 (I know it won't be a very detailed map but that's OK).
Many thanks to Francesco for his help!!!!!

Related

How to change lat/lng coordinates in pixel in Mapbox-gl-js inside Expression?

Good day!
I want to create an expression in Mapbox-gl-js for circle-radius. The challenge is circle-radius takes value in pixel but I have geographical coordinates(lat/lng).
When I try to use Map.project inside expression below(doesn't work):
['*',
2,
[
'*',
['pi'],
[
'-',
map.project(['get', 'p1'])[0], //<--doesn't work
map.project(['get', 'p2'])[0] //<--doesn't work
]
]
]
const layer: mapboxgl.Layer = {
'id': seatLayerName,
'type': "circle",
'source': 'maine',
'paint': {
"circle-radius": [
'interpolate',
['exponential', 2],
['zoom'],
5,
...//code
13,
['*', 2, ['*', ['pi'], ['-', ['get', 'p1'], ['get', 'p2']]]] //<-- How to use map.project here
]
}
};
Geojson:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
8.156983852386475,
-4.0492925407532
]
},
"properties": {
p1: [8.156983852386475, -4.0492925407532], //<-- point 1 lat/lng
p2: [8.176983852386475, -4.0492925407532] //<-- point 2 lat/lng
}
},
....
]
}
Attaching a screenshot, which I want to achieve creating an expression:
Mapbox provides map.project method to convert lat/lng to the pixel.
But how can I use this inside an expression?
Any suggestion would be helpful.

change layer property based on slider input with deck.gl

I am following an example provided on the deck.gl github repository that displays polygons from a geojson.
I've since changed the initial focus of the map and provided my own geojson to visualise, the data I've replaced the examples with has a temporal component that I'd like to visualise via the manipulation of a range input.
Example GeoJSON Structure
{
"type": "FeatureCollection",
"name": "RandomData",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature",
"properties": { "id": 1,"hr00": 10000, "hr01": 12000, "hr02": 12000, "hr03": 30000, "hr04": 40000, "hr05": 10500, "hr06": 50000}, "geometry": { "type": "Polygon", "coordinates": [ [ [ 103.73992, 1.15903 ], [ 103.74048, 1.15935 ], [ 103.74104, 1.15903 ], [ 103.74104, 1.15837 ], [ 103.74048, 1.15805 ], [ 103.73992, 1.15837 ], [ 103.73992, 1.15903 ] ] ] } } ] }
Instead of repeating each geometry for every timepoint I've shifted the temporal aspect of the data to the properties. This makes the file size manageable on the complete dataset (~50mb versus ~500mb).
For visualising a single time point I know that I can provide the property to getElevation and getFillColor.
_renderLayers() {
const {data = DATA_URL} = this.props;
return [
new GeoJsonLayer({
id: 'geojson',
data,
opacity: 0.8,
stroked: false,
filled: true,
extruded: true,
wireframe: true,
fp64: true,
getElevation: f => f.properties.hr00,
getFillColor: f => COLOR_SCALE(f.properties.hr00),
getLineColor: [255, 255, 255],
lightSettings: LIGHT_SETTINGS,
pickable: true,
onHover: this._onHover,
transitions: {
duration: 300
}
})
];
}
So I went ahead and used range.slider, adding code to my app.js, this following snippet was added. I believe I also may be placing this in the wrong location, should this exist in render()?
import ionRangeSlider from 'ion-rangeslider';
// Code for slider input
$("#slider").ionRangeSlider({
min: 0,
max: 24,
from: 12,
step: 1,
grid: true,
grid_num: 1,
grid_snap: true
});
$(".js-range-slider").ionRangeSlider();
added to my index.html
<input type="text" id="slider" class="js-range-slider" name="my_range" value=""/>
So how can I have the slider change which property of my geojson is being supplied to getElevation and getFillColor?
My JavaScript/JQuery is lacking and I have been unable to find any clear examples of how to change the data property based on the input, any help is greatly appreciated.
Here is a codesandbox link - doesn't seem to like it there however.
Locally with npm install and npm start should have it behave as intended.
At first you'll need to tell your dependent accessors about the value that is going to be changed by the slider. This can be done by using updateTriggers:
_renderLayers() {
const { data = DATA_URL } = this.props;
return [
new GeoJsonLayer({
// ...
getElevation: f => f.properties[this.state.geoJsonValue],
getFillColor: f => COLOR_SCALE(f.properties[this.state.geoJsonValue]),
updateTriggers: {
getElevation: [this.state.geoJsonValue],
getFillColor: [this.state.geoJsonValue]
}
// ...
})
];
}
And to actually change this value using range-slider you need to add onChange callback during the initialization:
constructor(props) {
super(props);
this.state = { hoveredObject: null, geoJsonValue: "hr01" };
this.sliderRef = React.createRef();
this._handleChange = this._handleChange.bind(this);
// ...
}
componentDidMount() {
// Code for slider input
$(this.sliderRef.current).ionRangeSlider({
// ...
onChange: this._handleChange
});
}
_handleChange(data) {
this.setState({
geoJsonValue: `hr0${data.from}`
});
}
render() {
...
<DeckGL ...>
...
</DeckGL>
<div id="sliderstyle">
<input
ref={this.sliderRef}
id="slider"
className="js-range-slider"
name="my_range"
/>
</div>
...
}
And this is basically it. And here is the full code

How to add new layer to client mapbox map on button click with JS/jQuery?

Problem:
I am working on project where I use node.js server that communicate with my spatial DB in PG and my client uses mapbox to vizualize map on his side. After hitting a button, the request is send to server, server to psql, psql to server as result query and then through socket.io back to client, where I want to put my geoJSON / new geometry as new layer on his map after that client buttonclick occurs. Map on client side in HTML is well working and I can interact with it. I use JS inside of HTML page of my client. From there I need to update mapbox map with new geometry after button click.
Code sample:
But I tried this code, but this do nothing after button click and will show no errors in devTool Chrome console:
<script>
mapboxgl.accessToken = 'secretToken-I-have-just-for-ilustr--this-is-working';
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/streets-v10',
center: [17.10, 48.14], // starting position on Bratislava
zoom: 11 // starting zoom
});
// Add zoom and rotation controls to the map.
map.addControl(new mapboxgl.NavigationControl());
// later SOCKET PROCESSING
$(document).ready(function(){
$('#buttonRun').click(function(e){
map.on('load', function () {
alert("got HERE") // this is working, alert shows itself
map.addLayer({
"id": "route",
"type": "line",
"source": {
"type": "geojson",
"data": {
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": [
[-122.48369693756104, 37.83381888486939],
[-122.48348236083984, 37.83317489144141],
[-122.48339653015138, 37.83270036637107],
[-122.48356819152832, 37.832056363179625],
[-122.48404026031496, 37.83114119107971],
[-122.48404026031496, 37.83049717427869],
[-122.48348236083984, 37.829920943955045],
[-122.48356819152832, 37.82954808664175],
[-122.48507022857666, 37.82944639795659],
[-122.48610019683838, 37.82880236636284],
[-122.48695850372314, 37.82931081282506],
[-122.48700141906738, 37.83080223556934],
[-122.48751640319824, 37.83168351665737],
[-122.48803138732912, 37.832158048267786],
[-122.48888969421387, 37.83297152392784],
[-122.48987674713133, 37.83263257682617],
[-122.49043464660643, 37.832937629287755],
[-122.49125003814696, 37.832429207817725],
[-122.49163627624512, 37.832564787218985],
[-122.49223709106445, 37.83337825839438],
[-122.49378204345702, 37.83368330777276]
]
}
}
},
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "#888",
"line-width": 8
}
});
});
});
});
</script>
Even this is not working - even if I set data in click function in a static way, but this data will later change dynamically. If I add that layer out of click event function scope, it is working and layer loads on client map.
Settings / versions:
Windows10 Pro - 64-bit
Google Chrome - Version 69.0.3497.100 (Official Build) (64-bit)
Node.js - v10.11.0
mapbox-gl.js v0.49.0
Q:
Is there any way how to add layer to mapbox map dynamically, please? And later to remove without page refresh? (I still not found answer, even here)
Solution
Okay, I found something I before did not see, concretely this.
I better read documentation, and it is impossible to setup new layers dynamically, but now it is working as follows / you need to:
Out of all scopes define your variables, for example geoJson1, and geoJson2 that you can later edit / fill with function
Setup your layer in advance on map with your ID (as in code below) and fill it for example with goeJson1 or with empty []
In your on click listener function call this: map.getSource('data-update').setData(geojson2);
You just can setup as many layers in advance as you need, and later you can update them.
Code result:
<script>
mapboxgl.accessToken = 'token-from-your-registered-account';
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/streets-v10',
center: [17.10, 48.14], // starting position on Bratislava
zoom: 11 // starting zoom
});
var geojson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-122.48369693756104, 37.83381888486939],
[-122.48348236083984, 37.83317489144141],
[-122.48339653015138, 37.83270036637107],
[-122.48356819152832, 37.832056363179625],
[-122.48404026031496, 37.83114119107971]
]
}
}]
};
var geojson2 = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[-122.48369693756104, 37.83381888486939],
[-122.48348236083984, 37.83317489144141],
[-122.48339653015138, 37.83270036637107],
[-122.48356819152832, 37.832056363179625],
[-122.48404026031496, 37.83114119107971],
[-122.48404026031496, 37.83049717427869],
[-122.48348236083984, 37.829920943955045],
[-122.48356819152832, 37.82954808664175],
[-122.48507022857666, 37.82944639795659],
[-122.48610019683838, 37.82880236636284],
[-122.48695850372314, 37.82931081282506],
[-122.48700141906738, 37.83080223556934],
[-122.48751640319824, 37.83168351665737],
[-122.48803138732912, 37.832158048267786],
[-122.48888969421387, 37.83297152392784],
[-122.48987674713133, 37.83263257682617],
[-122.49043464660643, 37.832937629287755],
[-122.49125003814696, 37.832429207817725],
[-122.49163627624512, 37.832564787218985],
[-122.49223709106445, 37.83337825839438],
[-122.49378204345702, 37.83368330777276]
]
}
}]
};
map.on('load', function () {
map.addLayer({
"id": "data-update",
"type": "line",
"source": {
"type": "geojson",
"data": geojson // your previously defined variable
},
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "#888",
"line-width": 8
}
});
});
$(document).ready(function(){
$('#buttonRun').click(function(e){
map.getSource('data-update').setData(geojson2);
});
});
</script>

How to style each Geometry of a GeoJSON GeometryCollection individually using Leaflet

I'm working on an interactive map showing the history of rail transport in a city using Leaflet and some GeoJSON data. My GeoJSON data looks like this:
var lines = [
{
"type": "FeatureCollection",
"name": "stadtmitte_gerresheim",
"startYear": "1902",
"endYear": "2019",
"lineColor": "#DD0000",
"trainID": "001",
"features": [
{
"type": "Feature",
"properties": {
"lineName": "U73",
"station1": "Universität Ost/Botanischer Garten",
"station2": "Gerresheim S",
"startYear": "2016",
"endYear": "2019",
"lineColor": "#DD0000",
},
"geometry": {
"type": "GeometryCollection",
"geometries": [
{
"type": "LineString",
"lineID": "001",
"lineDescription": "Schleife am S-Bahnhof Gerresheim",
"coordinates": [...
]
},
{
"type": "LineString",
"lineID": "002",
"lineDescription": "Gerresheim S bis Ecke Schönaustraße/Heyestraße",
"coordinates": [...
]
}
....
I'm working with several FeatureCollections, that consist of some Features. As you can see the geometry of each feature is defined by a GeometryCollection (that mostly comprises LineStrings).
I'd like to use Leaflet to style each single geometry (the LineStrings from the GeometryCollections) according to its members. At the moment I only manage to style the whole GeometryCollection via the Feature:
L.geoJSON(route, {
filter: function(feature) {
if (checkYear(feature)) {
return true;
} else {
return false;
}
},
style: function(feature) {
switch (feature.properties.tunnel) {
case 'yes': return {color: "#ff0000", dashArray: "1,6"};
default: return {color: "#ff0000"}
}
},
pointToLayer: function (feature, latlng) {
getIconSize();
switch (feature.properties.underground) {
case 'yes': return L.marker(latlng, {icon: stadtbahnIcon});
default: return L.marker(latlng, {icon: strassenbahnIcon});
}
}
}).addTo(mymap);
}
My question: Is it possible to hand individual style functions to different Geometries of the same Feature instead of handing the same style function to all Geometries in this Feature? And if so, how? I guess it should be possible, as Leaflet creates a single path for every Geometry.
Cheers
Is it possible to hand individual style functions to different Geometries of the same Feature?
No.
Leaflet creates one instance of L.Layer per Feature in the GeoJSON, right over here; and the application of the style callback function just loops over the L.Layers corresponding to each feature, it doesn't drill down on nested L.LayerGroups (also, the L.Polylines and L.Polygons created as the result of a GeometryCollection don't have a reference to the original GeoJSON feature nor its properties).
I guess you'll want to use TurfJS' geomEach (or something similar) in order to preprocess your features&geometries.

Mapbox geoJSON formatting

I am trying to make an interactive map using MapBox.
The basic idea is that people will hover over certain regions and they will light up and an event will trigger showing some content.
I got the map set up using Mapbox and then went ahead and got some data for the points by first
Going here to create a shapefile - http://www.gadm.org/country
And then going here to convert that file into a json file - http://www.mapshaper.org/
I then load the data into the map via mapbox and here is my result! (code below) - http://dev.touch-akl.com/nzmap/
As you can see thanks to the data I have downloaded it is almost a perfect trace of the country the only problem is that all the regions seem to be ONE object when hovering.
$.ajax({
url: '/nzmap/js/nzmap.json',
dataType: 'json',
success: function load(data) {
// set up the regions based on the JSON data and then call the styles and the hovers
var regions = L.geoJson(data, {
style: getStyle,
onEachFeature: onEachFeature
}).addTo(map);
// selects used to style the regionsLayer
function getStyle(feature) {
return {
weight: 2,
opacity: 0.1,
color: 'black',
fillOpacity: 0.7,
fillColor: 'red'
};
}
// combine the mouse in and mouse out?
function onEachFeature(feature, layer) {
layer.on({
mousemove: mousemove,
mouseout: mouseout
});
}
// mouse over change the layer styles
function mousemove(e) {
var layer = e.target;
// highlight feature
layer.setStyle({
weight: 3,
opacity: 0.3,
fillOpacity: 0.9
});
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
}
//mouse out reset the style
function mouseout(e) {
regions.resetStyle(e.target);
}
}
});
Here is a link to the json file if that helps understand what is going on - http://dev.touch-akl.com/nzmap//js/nzmap.json
It looks like some of the polygons are 'multipolygons' and some are seperate. I managed to create a new version of the map using just one region and it looks like this - http://dev.touch-akl.com/nzmap2/
My function for doing this is much the same except I have split the region off and use a json file that only has the coordinates for that region.
This is the JSON file for that example - http://dev.touch-akl.com/nzmap2/js/waikato.json
So where I am stuck is... Is there a way to format the original json file in a way that will keep each region as it's own separate object for hovers? And if so how can I go about changing the way I am setting up the map get the desired result?
It looks like I could make a seperate json file for each region like I am doing with the second example but that just does not seem like the right way to do this, been stuck for days so any help would be appreciated!
This is because what you are using isn't proper GeoJSON, at least not the GeoJSON L.GeoJSON expects. What you need is a GeoJSON FeatureCollection. A collection would look something like this:
[{
"type": "Feature",
"properties": {
"foo": "bar"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-104.05, 48.99],
[-97.22, 48.98],
[-96.58, 45.94],
[-104.03, 45.94],
[-104.05, 48.99]
]]
}
}, {
"type": "Feature",
"properties": {
"bar": "foo"
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-109.05, 41.00],
[-102.06, 40.99],
[-102.03, 36.99],
[-109.04, 36.99],
[-109.05, 41.00]
]]
}
}]
As you can see, a FeatureCollection contains individual features which can be or type Point, MultiPoint, LineString, MultiLineString, Polygon or MultiPolygon. If you use a FeatureCollection L.GeoJSON will work as you would expect. Take a look at the reference for FeatureCollection and L.GeoJSON. A tutorial/example on using them both can be found here.

Categories