MKCoordinateRegionMakeWithDistance gives wrong results on iphoneX - swift

When setting the region for a MKMapView using MKCoordinateRegionMakeWithDistance, the resulting region always gives the wrong results, where the size is always slightly bigger than the best fit I would get for other phone models.
for example, doing:
let region = MKCoordinateRegionMakeWithDistance(someLocation, 400, 200)
let adjustedRegion = mapView.regionThatFits(region)
mapView.setRegion(adjustedRegion, animated: true)
(The mapview's vertical and horizontal ratios are defined to be set to 2:1)
would always result in a view that would give me 420 m vertically, 210~ m horizontally, while this doesn't happen for other phone models.
Understandably, it is meant to find the 'best fit` region for the specified dimensions, what's concerning me is that the results are different on iPhone X specifically. (on models 8, 8+, 5s)
Is there something I need to do specifically for iPhone X models with mapViews?

Turns out, mapkit's mapView's MKCoordinateRegionMakeWithDistance does it's calculations without the safe area insets.
Since my mapView was set to be at the bottom of the screen, when applying the vertical distance, some reduction needs to be made to compensate for this weird behaviour.
let verticalDistance = 400 * ((mapView.bounds.height - mapView.safeAreaInsets.bottom) / mapView.bounds.height )
let region = MKCoordinateRegionMakeWithDistance(someLocation, verticalDistance, 200)
let adjustedRegion = mapView.regionThatFits(region)
mapView.setRegion(adjustedRegion, animated: true)
This allow the mapView's resulting region to be correct in vertical and horizontal distance (compared against google map's web distance measuring tool)

Related

Manually write world file (jgw) from Leaflet.js map

I have the need to export georeferenced images from Leaflet.js on the client side. Exporting an image from Leaflet is not a problem as there are plenty of existing plugins for this, but I'd like to include a world file with the export so the resulting image can be read into GIS software. I have a working script fort his, but I can't seem to nail down the correct parameters for my world file such that the resulting georeferenced image is located exactly correctly.
Here's my current script
// map is a Leaflet map object
let bounds = map.getBounds(); // Leaflet LatLngBounds
let topLeft = bounds.getNorthWest();
let bottomRight = bounds.getSouthEast();
let width_deg = bottomRight.lng - topLeft.lng;
let height_deg = topLeft.lat - bottomRight.lat;
let width_px = $(map._container).width() // Width of the map in px
let height_px = $(map._container).height() // Height of the map in px
let scaleX = width_deg / width_px;
let scaleY = height_deg / height_px;
let jgwText = `${scaleX}
0
0
-${scaleY}
${topLeft.lng}
${topLeft.lat}`
This seems to work well at large scales (ie zoomed in to city-level or so), but at smaller scales there is some distortion along the y-axis. One thing I noticed is that all examples of world files I can find (and those produced from QGIS or ArcMap) all have the x-scale and y-scale parameters being exactly equal (oppositely signed). In my calculations, these terms are different unless you are sitting right on the equator.
Example world file produced from QGIS
0.08984380916303301 // x-scale (size of px in x direction)
0 // rotation parameter 1
0 // rotation parameter 2
-0.08984380916303301 // y-scale (size of px in y direction)
-130.8723208723141056 // x-coord of top left px
51.73651369984968085 // y-coord of top left px
Example world file produced from my calcs
0.021972656250000017
0
0
-0.015362443783773333
-130.91308593750003
51.781435604431195
Example of produced image using my calcs with correct state boundaries overlaid:
Does anyone have any idea what I'm doing wrong here?
Problem was solved by using EPSG:3857 for the worldfile, and ensuring the width and height of the map bounds was also measured in this coordinate system. I had tried using EPSG:3857 for the worldfile, but measured the width and height of the map bounds using Leaflet's L.map.distance() function. To solve the problem, I instead projected corner points of the map bounds to EPSG:3857 using L.CRS.EPSG3857.project(), the simply subtracted the X,Y values.
Corrected code is shown below, where map is a Leaflet map object (L.map)
// Get map bounds and corner points in 4326
let bounds = map.getBounds();
let topLeft = bounds.getNorthWest();
let bottomRight = bounds.getSouthEast();
let topRight = bounds.getNorthEast();
// get width and height in px of the map container
let width_px = $(map._container).width()
let height_px = $(map._container).height()
// project corner points to 3857
let topLeft_3857 = L.CRS.EPSG3857.project(topLeft)
let topRight_3857 = L.CRS.EPSG3857.project(topRight)
let bottomRight_3857 = L.CRS.EPSG3857.project(bottomRight)
// calculate width and height in meters using epsg:3857
let width_m = topRight_3857.x - topLeft_3857.x
let height_m = topRight_3857.y - bottomRight_3857.y
// calculate the scale in x and y directions in meters (this is the width and height of a single pixel in the output image)
let scaleX_m = width_m / width_px
let scaleY_m = height_m / height_px
// worldfiles need the CENTRE of the top left px, what we currently have is the TOPLEFT point of the px.
// Adjust by subtracting half a pixel width and height from the x,y
let topLeftCenterPxX = topLeft_3857.x - (scaleX / 2)
let topLeftCenterPxY = topLeft_3857.y - (scaleY / 2)
// format the text of the worldfile
let jgwText = `
${scaleX_m}
0
0
-${scaleY_m}
${topLeftCenterPxX}
${topLeftCenterPxY}
`
For anyone else with this problem, you'll know things are correct when your scale-x and scale-y values are exactly equal (but oppositely signed)!
Thanks #IvanSanchez for pointing me in the right direction :)

leaflet editable restrict draw to a specific area

In Leaflet.Editable I want to confine/limit my customers to draw only in a specific area/bounds.
actually im trying to limit them to (90, -90, 180, -180) bounds of map..
maxBounds: [[-90, -180], [90, 180]]
I was not able to find anything anywhere and it seems that i am missing something.
CODEPEN DEMO
please help.
EDIT:
the Y axis is blocking correctly and mouse cannot stretch shape beyond top and bottom.
the problem is in X axis (as seen in pictures)
as for now i solved it with after save check and clear shape if it out of map bounds (BAD USER EXPERIENCE). i need a mouse confinement just like y axis does.
Without knowing your use case (why the whole world map??) Quickest and easiest fix would be to simply set the map's minZoom to something a bit higher, for example, I found that minZoom: 5 was adequate except for cases where the map was both really short and really wide (which is rarely the case in most apps I've seen).
But the real fix involves writing your own custom overrides for dragging markers and shapes.
According to API doc the L.Editable plugin allows you to override a bunch of stuff including the VertexMarker class, via map.editTools.options.vertexMarkerClass.
Fixed codepen: http://codepen.io/anon/pen/GrPpRY?editors=0010
This snippet of code that allows you to constrain the longitude for dragging vertex markers by correcting values under -180 and over 180 is this:
// create custom vertex marker editor
var vertexMarkerClass = L.Editable.VertexMarker.extend({
onDrag: function(e) {
e.vertex = this;
var iconPos = L.DomUtil.getPosition(this._icon),
latlng = this._map.layerPointToLatLng(iconPos);
// fix out of range vertex
if (latlng.lng < -180) {
e.latlng.lng = latlng.lng = -180;
this.setLatLng(latlng);
}
if (latlng.lng > 180) {
e.latlng.lng = latlng.lng = 180;
this.setLatLng(latlng);
}
this.editor.onVertexMarkerDrag(e);
this.latlng.update(latlng);
this._latlng = this.latlng; // Push back to Leaflet our reference.
this.editor.refresh();
if (this.middleMarker) this.middleMarker.updateLatLng();
var next = this.getNext();
if (next && next.middleMarker) next.middleMarker.updateLatLng();
}
});
// attach custom editor
map.editTools.options.vertexMarkerClass = vertexMarkerClass;
I didn't code for dragging the shape as a whole (the rectangle, in this case). While the VertexMarker fix should address all kinds of vertex dragging, you need to override each shape's drag handler to properly constrain the bounds. And if bounds are exceeded, crop the shape appropriately. As was pointed out, Leaflet already does this for latitude, but because Leaflet allows wrapping the map around horizontally you have your essential problem. Using rec.on("drag") to correct the bounds when they cross over your min/max longitude is the only way to address it. It is basically the same solution as I have laid out for the vertexMarkerClass - actual code left as exercise for the diligent reader.

Understanding function algorithm

During study tutorial, i using function to show visible rect, containing all of annotations (on mapView) as it shown:
// 1
let rectToDisplay = self.treasures.reduce(MKMapRectNull) { (mapRect: MKMapRect, treasure: Treasure) -> MKMapRect in
// 2
let treasurePointRect =
MKMapRect(origin: treasure.location.mapPoint,
size: MKMapSize(width: 0, height: 0))
return MKMapRectUnion(mapRect, treasurePointRect)
// 3
}
// 4
self.mapView.setVisibleMapRect(rectToDisplay, edgePadding: UIEdgeInsetsMake(74, 10, 10, 10), animated: false)
Everything work, but I'm not understanding how exactly it working.
On third lane, we are doing following:
return MKMapRectUnion(mapRect, treasurePointRect)
Before that, we declare
mapRect: MKMapRect
So, mapRect does not have any initial value and suppose to not contain any values. Am i right?
How exactly MKMapRectUnion calculated, if mapRect have zero values? Is there any way i could look at every step of function using some kind of NSLog statement?
If you be so kind, please, explain me in details how that function work. As i understand, function try to make map Rect combining "zero" value map point and other mapPoint, with correct values.
you have understood everything correctly. as for your question
How exactly MKMapRectUnion calculated, if mapRect have zero values?
apple docs says:
If either rectangle is null, this method returns the other rectangle.
The origin point of the returned rectangle is set to the smaller of
the x and y values for the two rectangles. Similarly, the size and
width of the rectangle are computed by taking the maximum x and y
values and subtracting the x and y values for the new origin point.
how exactly that is being calculated, you can ask apple engineers or reverse engineer the mapkit. if the function does work fine then you don't have go to such length and divert from your real assignment.

Coordinates of bottom-left corner of a mapView off by a few pixels

I'm trying to put an Annotation the lower left corner of currently visible map region, although the following code
CLLocationCoordinate2D origin = getOriginOfRegion(mapView.region);
[[[SimpleAnnotation alloc] initWithCoords:origin] autorelease];
...
extern CLLocationCoordinate2D getOriginOfRegion(MKCoordinateRegion region){
return CLLocationCoordinate2DMake(region.center.latitude-region.span.latitudeDelta/2,
region.center.longitude-region.span.longitudeDelta/2);
}
where SimpleAnnotation is as simple as it gets; puts the pin point a few degrees off the actual corner:
On the right side zoom level is higher, and as you can see the mistake is significantly smaller.
I've tried doing some math to counter it, considering that the error may be something related to projecting elliptical coordinates onto a plane, but didn't come up with anything useful.
Has anyone solved this problem? I've noticed that latitude values are not 'linear' on a mapView, I need some hints to understand how to transform them.
I don't know if this will work but I suggest you try letting the map view convert view coordinates to map coordinates with convertPoint:toCoordinateFromView:.
For example:
CGPoint bottomLeftPoint = CGPointMake(CGRectGetMinX(map.bounds), CGRectGetMaxY(map.bounds));
CLLocationCoordinate2D bottomLeftCoord = [map convertPoint:bottomLeftPoint toCoordinateFromView:map];

Why MKCoordinateSpan changes?

When I debug code below, I see that span is changed by mapkit from what I have provided. span2 contains different numbers from what was provided. Why is this happening?
- (void) viewDidLoad
{
[super viewDidLoad];
CLLocationCoordinate2D loc;
loc.latitude = self.atm.lat;
loc.longitude = self.atm.lon;
MKCoordinateSpan span1 = MKCoordinateSpanMake(0.05f, 0.05f);
self.mapView.region = MKCoordinateRegionMake(loc, span1);
// at this point numbers are not 0.05 anymore
MKCoordinateSpan span2 = self.mapView.region.span;
// ... more code
}
Note that latitude and longitude change differently so square MapView will actually display region with different span values for its coordinates. This is likely to cause your mapview region to have span (slightly?) different from what you set.
In reference for MKMapView's region property there's somewhat relevant phrase:
Changing only the center coordinate of the region can still
cause the span to change implicitly.
This is due to the fact that the
distances represented by a span change
at different latitudes and longitudes
and the map view may need to adjust
the span to account for the new
location.
A square MapView with identical values for the latitudinal and longitudinal span will almost always experience the change in span indicated above as longitudinal arc-length varies significantly as you move from the equator to either pole. As the latitude approaches +/-90, the longitudinal arc-length approaches 0.
In addition, however, identical span values will also change if the MapView region is not square (as the span only refers to the region actually visible at the time).