Turf.js and PostGIS 'difference' function giving unexpected result - leaflet

I have a polygon and a multipolygon. I want to create a geometry that is the area where the 2 geometries do not intersect. The result I get (red outline in the example) seems to have something like an extra point in the southmost and northmost part of the geometries and I don't understand why.
Can anyone help me figure out how I can get the result of having the red outline exactly around the purple area?
As a side note, I can get the result I'm looking for if I use PostGIS and do exactly ST_Difference(ST_SnapToGrid(poly1, 0.0000000001), ST_SnapToGrid(poly2, 0.0000000001)) from MYTABLE; however I don't comprehend why this works and it seems like it may be a brittle solution. Any decimal difference in 0.0000000001 doesn't work (0.000001 for example gives me the same result as the turfjs result).
var map = L.map("map").setView([19.42, -155], 9);
L.tileLayer('https://stamen-tiles.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var poly1 = turf.polygon([[[-155.253325763,19.424193039],[-155.140558658,19.206650122],[-154.752533025,19.515649896],[-154.933670824,19.645287706],[-155.253325763,19.424193039]]]);
var poly2 = turf.multiPolygon([[[[-154.975609356,19.338005611],[-155.140558658,19.206650122],[-155.240053913,19.398589856],[-155.021104033,19.34867505],[-154.975609356,19.338005611]]],[[[-154.9542326,19.63106581],[-154.933670824,19.645287706],[-154.904729907,19.624575093],[-154.9542326,19.63106581]]]]);
var difference = turf.difference(poly1, poly2);
var myStyle = {weight: 0, fillOpacity: 0.3, fillColor: '#0000ff'};
L.geoJSON(poly1, {style: myStyle}).addTo(map);
myStyle.fillColor = '#00ff00';
L.geoJSON(poly2, {style: myStyle}).addTo(map);
myStyle = {weight: 1, opacity: 1, color: 'red', fillOpacity: 0.05, fillColor: '#ff0000'};
L.geoJSON(difference, {style: myStyle}).addTo(map);
#map {
height: 200px;
}
<script src="https://cdn.jsdelivr.net/npm/#turf/turf#6.5.0/turf.min.js"></script>
<script src="https://unpkg.com/leaflet#1.0.3/dist/leaflet.js"></script>
<link href="https://unpkg.com/leaflet#1.0.3/dist/leaflet.css" rel="stylesheet"/>
<div id="map"></div>

Precision are a common problem when working with geometries. Have to use ST_SnapToGrid or other techniques to fix topological issues before use a geoprocess is no rare
So answering one of your questions, use ST_SnapToGrid is not a bad solution. For sure it will be better that the original data exactly match but that is not always possible.
Also, just a matter of taste, but usually I found more "understable" dump multi geometries to simple geometries and operate on that, that working directly with the multi. I find easy to debug problems when using simple versions.
To fix the precision issue in turf you can try to play with truncate
var options = {precision: 4};
var poly1 = turf.truncate(poly1, options);
var poly2 = turf.truncate(poly2, options);

Related

Leaflet lowest zoom level is still too high with L.CRS.Simple

Trying to retrieve part of a district, however for some reason cannot see the whole area, even if zoom level is at 0, where (supposedly) we should see the whole world.
I am using L.CRS.Simple because this uses the EPSG:3763 and cannot see that one on the CRS list. I am retrieving the data in JSON cause when tying with geoJSON, was not able to transform the 3D coordinates data into 2D planes ones.
const queryRegionText = "where=OBJECTID > 0"
const geoJsonURL2 = "https://sig.cm-figfoz.pt/arcgis/rest/services/Internet/MunisigWeb_DadosContexto/MapServer/2/query?f=json&returnGeometry=true&geometryType=esriGeometryPolyline&spatialRel=esriSpatialRelIntersects&outFields=*&outSR=3763&" + queryRegionText
var map = L.map('mapid', {
crs: L.CRS.Simple
}).setView([-58216.458338, 42768.347232], 0);
L.control.scale({ metric: true }).addTo(map);
fetch(geoJsonURL2).then(function (response) {
response.json().then(function (data) {
data.features.forEach(element => {
if (element.geometry.rings) {
element.geometry.rings.forEach(point => {
L.polyline(point, { color: 'red' }).addTo(map);
})
}
});
});
});
var popup = L.popup();
function onMapClick(e) {
popup
.setLatLng(e.latlng)
.setContent("You clicked the map at " + e.latlng.toString())
.openOn(map);
}
map.on('click', onMapClick);
<html>
<head>
<title>Leaflet - testing</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet#1.7.1/dist/leaflet.js"></script>
</head>
<body>
<div id="mapid" style="width: 600px; height: 400px;"></div>
</body>
</html>
TL;DR: When creating the map, set the minimum zoom below zero. This should work:
var map = L.map('mapid', {
crs: L.CRS.Simple, minZoom: -6
}).setView([-57728, 55296], -6);
Explanation
Normally, Leaflet translates from a latitude/longitude coordinate system to screen pixels using an assumption that the world is 256 pixels high at Zoom level 0. At each higher Zoom Level, the number of pixels doubles (explained nicely in the Zoom levels tutorial). With this assumption, the options for the map default to {minZoom: 0, maxZoom: Infinity} (as you are not adding any Layer that sets these values to anything different).
When you use L.CRS.Simple, at Zoom level 0 it maps 1 coordinate unit to 1 screen pixel. Your data looks like it is about 18000 coordinate units tall, so it doesn't fit in your 400 pixel high map. To make it fit, we need each screen pixel to map to about 45 coordinate units. 2^5 is 32, and 2^6 is 64, so we need to zoom out between 5 and 6 times. Luckily, Leaflet accepts negative Zoom Levels, so setting zoom to -6 does the trick. But to make it work properly, you need to set {minZoom: -6}, so the map doesn't get stuck at zoom level 0. There's a good worked example in the Non-geographical Maps tutorial.
Using L.CRS.Simple should work for you, so long as the approximation holds that each latitude unit is the same length as each longitude unit (a square world). Since this isn't generally true in the real world, using the Simple projection will cause some distortion. If the distortion is significant for the features you are interested in, then you will need to look up how to use EPSG:3763 properly, using L.CRS and Proj4Leaflet, as suggested by #IvanSanchez.
So, after some reading on the proj4leaflet, come up with this code. Thanks in advance for the comments and the reply above.
const queryRegionText = "where=OBJECTID > 0"
const geoJsonURL2 = "https://sig.cm-figfoz.pt/arcgis/rest/services/Internet/MunisigWeb_DadosContexto/MapServer/2/query?f=geojson&returnGeometry=true&geometryType=esriGeometryPolyline&spatialRel=esriSpatialRelIntersects&outFields=*&outSR=3763&" + queryRegionText
const map = L.map('map', {
center: [40.14791, -8.87009],
zoom: 13
});
proj4.defs("EPSG:3763", "+proj=tmerc +lat_0=39.66825833333333 +lon_0=-8.133108333333334 +k=1 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs");
fetch(geoJsonURL2).then(function (response) {
response.json().then(function (data) {
L.Proj.geoJson(data).addTo(map);
});
});
var popup = L.popup();
function onMapClick(e) {
popup
.setLatLng(e.latlng)
.setContent("You clicked the map at " + e.latlng.toString())
.openOn(map);
}
map.on('click', onMapClick);
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.7.1/dist/leaflet.css" />
<link rel="stylesheet" href="main.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.4/proj4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4leaflet/1.0.2/proj4leaflet.js"></script>
</head>
<body class="">
<div id="map" style="width: 600px; height: 400px;"></div>
</div>
<script src="main.js"></script>
</body>

Using minZoom with map.fitBounds without getting error

I have a Leaflet map that I am trying to plot multiple points using map.fitbounds() but I would like the set a minZoom so the user cant zoom all the way out past a zoom level of 9. I currently use the below to add pins to a map except sometimes when a map as multiple points where the map would need to fitbounds at a wider zoom it causes the map to fail. When the map fitbounds is set higher than 9 then it works fine but below 9 it doesnt plot.
I was wondering if someone would know how to both safeguard a map from failing with a minZoom and using fitBounds in certain instances and to also stop a user from zooming so far out.
map.options.minZoom = 9;
map.options.maxZoom = 15;
var fg = L.featureGroup().addTo(map);
for ( var i=0; i < markersArray.length; ++i )
{
var linkid = (markersArray[i]['linkid']);
var linkurl = (markersArray[i]['linkurl']);
L.marker( [markersArray[i].lat, markersArray[i].lng], {icon: myIcon} )
.bindPopup( '<div id="mapPinDetails"><h3>' + markersArray[i].name + '</h3>' + markersArray[i].address + '<br /></div>' )
.addTo(fg);
}
map.fitBounds(fg.getBounds());
below 9 it doesnt plot
It does, but since you do not allow your map to zoom out below 9, it remains at zoom level 9 and you may not see your outer markers. But if you pan you can see them. The view center is the same that would have been achieved with a lower zoom:
var map = L.map('map', {
minZoom: 9,
maxZoom: 15,
}).setView([48.86, 2.35], 11);
var fg = L.featureGroup().addTo(map);
L.marker([49, 2.35]).addTo(fg);
L.marker([48.5, 2.35]).addTo(fg);
map.fitBounds(fg.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.1/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.3.1/dist/leaflet-src.js" integrity="sha512-IkGU/uDhB9u9F8k+2OsA6XXoowIhOuQL1NTgNZHY1nkURnqEGlDZq3GsfmdJdKFe1k1zOc6YU2K7qY+hF9AodA==" crossorigin=""></script>
<div id="map" style="height: 180px"></div>
If you need further help, you will have to explain what you mean by "it causes the map to fail" and if possible share a reproducible example.

Fit map to bounds exactly

Is there a way to fit the map exactly to bounds? (or as close a possible).
For example, in this jsfiddle, even without padding the map leaves a lot of padding above and below the points:
http://jsfiddle.net/BC7q4/444/
map.fitBounds(bounds);
$('#fit1').click(function(){ map.fitBounds(bounds); });
$('#fit2').click(function(){ map.fitBounds(bounds, {padding: [50, 50]}); });
I'm trying to fit a map as precisely as possible to a set of bounds that closely match the map shape without all the extra padding.
Setting a map's ne corner and sw corner would work as well, but I don't think that functionality exists.
You are very probably looking for the map zoomSnap option:
Forces the map's zoom level to always be a multiple of this, particularly right after a fitBounds() or a pinch-zoom. By default, the zoom level snaps to the nearest integer; lower values (e.g. 0.5 or 0.1) allow for greater granularity. A value of 0 means the zoom level will not be snapped after fitBounds or a pinch-zoom.
Because its default value is 1, after your fitBounds the map will floor down the zoom value to the closest available integer value, hence possibly introducing more padding around your bounds.
By specifiying zoomSnap: 0, you ask the map not to snap the zoom value:
var map = L.map('map', {
zoomSnap: 0 // http://leafletjs.com/reference.html#map-zoomsnap
}).setView([51.505, -0.09], 5);
// add an OpenStreetMap tile layer
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var southWest = new L.LatLng(40.712, -74.227),
northEast = new L.LatLng(40.774, -74.125),
bounds = new L.LatLngBounds(southWest, northEast);
L.marker([40.712, -74.227]).addTo(map);
L.marker([40.774, -74.125]).addTo(map);
map.fitBounds(bounds);
$('#fit1').click(function() {
map.fitBounds(bounds);
});
$('#fit2').click(function() {
map.fitBounds(bounds, {
padding: [50, 50]
});
});
body {
padding: 0;
margin: 0;
}
#map {
height: 300px;
margin-top: 10px;
}
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.2.0/dist/leaflet.css">
<script src="https://unpkg.com/leaflet#1.2.0/dist/leaflet-src.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<button id="fit1">fit without bounds</button>
<button id="fit2">fit with bounds</button>
<div id="map"></div>

GeoJSON won't show on Leaflet (1.0)

I'm trying to show a some GeoJSON on my map, and it isn't working. I'm pulling complex multipolygons from the database, but for simplicity's sake, I tried constructing a basic polygon by hand. I can't get it to show either.
I tried creating a new pane with a zIndex of 10000 and adding the layer there to no avail. I tried swapping lat/long in case I somehow reversed them, and that didn't work either. I'm using a 10px weight to ensure there's no way it could be missed.
Here is the basic example where I'm just trying to add a polygon that bounds Manhattan to a map.
var mapDivId = 'map';
var leafletMap = L.map(mapDivId,
{'boxZoom': true,
'doubleClickZoom': false,
'scrollWheelZoom': true,
'touchZoom': true,
'zoomControl': true,
'dragging': true});
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
});
osm.addTo(leafletMap);
var districtOutlineStyle = {
"color": "black",
"weight": "10px",
"opacity": "0.5"
};
var overlaySimplePolygonAroundManhattan = function() {
xmin = 40.68291;
xmax = 40.87903;
ymin = -74.04772;
ymax = -73.90665;
geoJsonShape = {'type': "Polygon",
'coordinates': [[[xmin, ymin], [xmin, ymax], [xmax, ymax], [xmax, ymin], [xmin, ymin]]]};
districtsOverlay = L.geoJSON(geoJsonShape,
{style: districtOutlineStyle});
leafletMap.addLayer(districtsOverlay);
};
overlaySimplePolygonAroundManhattan();
Any idea why it won't display the polygon? This is Leaflet 1.1.0.
Thanks!
Two things should help:
Your xMin/Max are mixed up with you yMin/Max:
So if you are zoomed in to start, you won't find your polygon unless zooming out to Antarctica.
xmin = 40.68291; // 40 degrees north, not east
xmax = 40.87903;
ymin = -74.04772; // 74 degrees west, not south
ymax = -73.90665;
You don't need to specify pixels, leaflet is expecting a number when styling the polygon. Leaflet doesn't handle this error gracefully apparently, resulting in no visible polygon.
(I also set the initial zoom and centering location) Altogether, this gives a result like:
var mapDivId = 'map';
var leafletMap = L.map(mapDivId,
{'boxZoom': true,
'doubleClickZoom': false,
'scrollWheelZoom': true,
'touchZoom': true,
'zoomControl': true,
'dragging': true}).setView([40.75, -73.97], 10);
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
});
osm.addTo(leafletMap);
var districtOutlineStyle = {
"color": "black",
"weight": 10,
"opacity": "0.5"
};
var overlaySimplePolygonAroundManhattan = function() {
ymin = 40.68291;
ymax = 40.87903;
xmin = -74.04772;
xmax = -73.90665;
geoJsonShape = {"type": "Polygon",
"coordinates": [[[xmin, ymin],[xmax, ymin], [xmax, ymax], [xmin, ymax], [xmin, ymin]]]};
districtsOverlay = L.geoJSON(geoJsonShape, {style: districtOutlineStyle});
leafletMap.addLayer(districtsOverlay);
// Alternative approach:
//L.geoJSON(geoJsonShape,{style: districtOutlineStyle}).addTo(leafletMap);
};
overlaySimplePolygonAroundManhattan();
#map {
width: 600px;
height: 400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0/leaflet.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0/leaflet.css"/>
<div id="map"></div>
Andrew is right! It was because I specified pixels instead of just a number for the weight. It works great now.
I had tried swapping lat/long multiple times, so I knew it wasn't just that. And that was debugging code. I thought the code snippet I posted had them in the right order but apparently not. The real shapes I'm displaying are MultiPolygons pulled as GeoJSON from Postgres, so I was confident I had the proper lat/long ordering there, even if they were backwards in my debugging function. As soon as I removed 'px', my function to display the real shapes worked too.
Thank you!

Leaflet polylines not working

I'm trying to show Polylines in Leaflet but it doesn't seem to work. Am I missing anything or...? p.s. Coordinate pairs do not contain the same values, so it should yield lines...
//this adds markers to the map, which works
var d = {};
d.coordinates = [[lat,lng],[lat,lng]]
var latLngs = d.coordinates.map(function (c) {
var marker = L.marker(c).addTo(map);
return {
lat: c[0],
lng: c[1]
};
});
//This 'should' add polylines but doesn't ...
var polyline = L.polyline(d.coordinates, { color: 'red', weight: 12 }).addTo(map);
I tried all sorts of variations on the code above, varying with instantiation/factory method like: new L.polyline() vs L.polyline() and trying upper- and lowercase Polyline. I tried passing arrays of [double, double] and [L.LatLng, L.LatLng] and even [{lat:lat,lng:lng}] but nothing seems to work... I really must be overlooking a simple thing...
I'm using Leaflet 0.7.
Edit:
As shown in the jsFiddle by ghybs it should work. I updated my code to the following:
var firstpolyline = L.polyline(d.coordinates, {
color: 'red',
weight: 12
}).addTo(map);
I also added identical logging statements in both the jsFiddle and my solution.
console.log(firstpolyline); //polyline for jsFiddle
console.log(map);
console.log(d.coordinates);
Those yield this (left is custom solution which is not showing a line, right is jsFiddle which is showing a line). Manually copy-pasting different coordinates pairs of my solution to the jsFiddle just works...:
I'm really lost here :(
There is no reason to add an extra .polyline after the factory L.polyline().
var polyline2 = L.polyline(d.coordinates, {color: 'red', weight: 12}).addTo(map);
Demo: http://jsfiddle.net/ve2huzxw/48/
Side note: you should use a single equal sign (=) for assignment in d.coordinates == [[lat,lng],[lat,lng]]