Inverted Y axis of custom tile images names - javascript

There is a know problem of Leaflet that when you use a custom tile provider, not with real earth images, set crs: L.CRS.Simple, Leaflet queries for images where Y coordinate is inverted in comparison to the math axis. So the first top-right image's location is 1x-1 instead of 1x1.
In the internet topics about inverting Y axis are rather old, so my question is: nowadays is there a normal short and built-in way to invert queried Y axis?
The only old solutions I've found were rewriting Leaflet internal objects, like extending L.CRS.Simple.

As noted in the Leaflet tutorial for WMS/TMS, the canonical way of inverting the Y coordinate for tile coordinates is using {-y} instead of {y} in the tile URL template. e.g.:
var layer = L.tileLayer('http://base_url/tms/1.0.0/tileset/{z}/{x}/{-y}.png');
Note, however, that (as of Leaflet 1.3.1) that only works for maps with a non-infinite coordinate system.
In your case, you might want to get around this by creating your own subclass of L.TileLayer. There is a complete guide on the Leaflet tutorial about extending layers, but the TL;DR version for a tilelayer that shifts its tile coordinates is:
L.TileLayer.CustomCoords = L.TileLayer.extend({
getTileUrl: function(tilecoords) {
tilecoords.x = tilecoords.x + 4;
tilecoords.y = tilecoords.y - 8;
tilecoords.z = tilecoords.z + 1;
return L.TileLayer.prototype.getTileUrl.call(this, tilecoords);
}
});
var layer = new L.TileLayer.CustomCoords(....);
And the specific case for just inverting the Y coordinate is:
L.TileLayer.InvertedY = L.TileLayer.extend({
getTileUrl: function(tilecoords) {
tilecoords.y = -tilecoords.y;
return L.TileLayer.prototype.getTileUrl.call(this, tilecoords);
}
});
var layer = new L.TileLayer.InvertedY(....);

Related

How to use Leaflet flyTo() with unproject() and GeoJSON data on a large raster image?

I'm building a story map with Leaflet using a large image sliced into tiles rather than 'real world' map data. I'm using this plugin: https://commenthol.github.io/leaflet-rastercoords/ and this repo: https://github.com/jackdougherty/leaflet-storymap
Loading my GeoJSON data and unprojecting the coordinates correctly plots them on my image map:
$.getJSON('map.geojson', function(data) {
var geojson = L.geoJson(data, {
// correctly map the geojson coordinates on the image
coordsToLatLng: function (coords) {
return rc.unproject(coords)
},
But when I get to onEachFeature, I hit the wall with map.flyTo(), which is calling geometry.coordinates from my JSON file, but not unprojecting them so flyTo() is interpreting them as geospatial coordinates way off the map:
map.flyTo([feature.geometry.coordinates[1], feature.geometry.coordinates[0] ], feature.properties['zoom']);
I tried passing the unprojected coordinates to variables and then to map.flyTo() and variations on nesting functions, such as map.flyTo.unproject(..., but no luck.
How do I pass my raster coordinates to flyTo()?
I'm not only new to Leaflet, but new to JavaScript. I hacked my way this far, but I'm stumped. I'm sure the solution is obvious. Any help is greatly appreciated.
In your case you would probably just need to use rc.unproject to convert your coordinates into LatLng that you can pass to flyTo:
map.flyTo(
rc.unproject(feature.geometry.coordinates),
feature.properties['zoom']
);
That being said, I must admit I do not exactly see the point of using leaflet-rastercoords plugin, since you can easily do the same by following the Leaflet tutorial "Non-geographical maps".
var yx = L.latLng;
var xy = function(x, y) {
if (L.Util.isArray(x)) { // When doing xy([x, y]);
return yx(x[1], x[0]);
}
return yx(y, x); // When doing xy(x, y);
};
With this, whenever you want to convert your "raster" coordinates into something usable by Leaflet, you would just use xy(x, y) with x being your horizontal value, and y your vertical one.
The added benefit is that many other things will become easily compatible.
Since you use tiles instead of a single image (that is stretched with ImageOverlay in the tutorial in order to fit the coordinates), you should modify the CRS transformation, so that at zoom 0, your tile 0/0/0 fits your entire coordinates. See also Leaflet custom coordinates on image
I.e. in the case of leaflet-rastercoords example:
Original raster image size: 3831 px width x 3101 px height
Tiles size: 256 x 256 px
Vertical "raster" coordinates are increasing while going down (whereas in the Leaflet tutorial, they increase going up, like latitude).
Tile 0/0/0 actually covers more than the original image, as if the latter were 4096 x 4096 px (the rest is filled with white)
Determination of the new CRS:
Tile 0/0/0 should cover coordinates from top-left [0, 0] to bottom-right [4096, 4096] (i.e. 256 * 2^4 = 256 * 16 = 4096) => transformation coefficients a and c should be 1/16
No offset needed => offsets b and d are 0
No reversion of y vertical coordinate => c is positive
Therefore the new CRS to be used would be:
L.CRS.MySimple = L.extend({}, L.CRS.Simple, {
// coefficients: a b c d
transformation: new L.Transformation(1 / 16, 0, 1 / 16, 0)
});
Now your flyTo is very similar, but many other things are also compatible:
map.flyTo(
xy(feature.geometry.coordinates),
feature.properties['zoom']
);
Demo adapted from leaflet-rastercoords example, and using an extra plugin to demonstrate compatibility: https://plnkr.co/edit/Gvei5I0S9yEo6fCYXPuy?p=preview

Draw polygons on existing SVG to capture coordinate data

We have a web application that displays a SVG map of an office. The map has small icons that represent users walking around with RF tags. This allows administrators of the system to see what rooms users are in. We are using Snap.SVG to load the office SVG file and manipulate it to display the user icons. The challenge is that the map scales to the size of the browser. Using JavaScript to determine the coordinates is not always accurate because the position of the SVG changes based on the browser size.
Here is an example of the map with the icons:
The icons are placed on the map based on X Y coordinates coming from our database. The values for the X Y coordinates are set for each location and were determined using Adobe Illustrator. Currently, we can only place one icon in a room at a time. Because we only have 1 set coordinates the icons overlap if more than one person is in a room at one time.
The second phase of this project is to allow users to draw on of the map to specify locations. Essentially, the user will set points and create a polygon to represent each location on the map. We would use the coordinates of the polygon along with the total area of the polygon to know where on the map we can place icons. This would allow users to define areas without a developer getting involved.
Here is an example of what we want to achieve .
I have been researching how to do this, but have not found anything outside of using something like the Google Maps API to draws polygons on a map. I did find this article that outlines how to dynamically pull points. We thought about using a grid system that is an overlay on the map and the user defines what grid elements are in what locations. So something like [A1,A2,B1,B2]. I persoanlly like the polygon approach as it is more visually appealing and is easier for a user to adopt.
We need some advice on where to start with this and if something like snap.svg is all we need or if we have to rely on other libraries in conjunction with snap.
Update:
With Ian's advice I found a fiddle that describes what he was talking about.
var S;
var pt;
var svg
var box;
window.onload = function(){
svg = $('#mysvg')[0];
S = Snap(svg);
console.log( S );
pt = pt = svg.createSVGPoint(); // create the point
// add the rectangle
box = S.rect(12,12, 12, 12);
box.attr({ fill : 'red', stroke : 'none' });
S.drag(
function(dx, dy, posX, posY, e){
//onmove
pt.x = posX - S.node.offsetLeft;
pt.y = posY - S.node.offsetTop;
console.log(pt.x + "," + pt.y);
// convert the mouse X and Y
//so that it's relative to the svg element
var transformed = pt.matrixTransform(svg.getCTM().inverse());
box.attr({ x : transformed.x, y : transformed.y });
},
function(){
//onstart
},
function(){
//onend
}
);
}
The Fiddle

How can I convert a google maps circle into GeoJSON?

I need to convert a google maps Circle into GeoJSON. GeoJSON doesn't support circles so I want to generate an N-sided polygon. I found a nice method for generating a regular N-sided polygon but that requires the radius to be defined in the coordinate system while the radius of a circle in Google Maps is defined in meters.
Google Maps has a handy set of spherical functions in the geometry library that makes this really easy for us. Specifically, we want the computeOffset function:
Returns the LatLng resulting from moving a distance from an origin in the specified heading (expressed in degrees clockwise from north).
We have the origin (the center of the circle) and a distance (the radius of the circle), so we just need to calculate a set of headings for the points based on how many sides we want.
function generateGeoJSONCircle(center, radius, numSides){
var points = [],
degreeStep = 360 / numSides;
for(var i = 0; i < numSides; i++){
var gpos = google.maps.geometry.spherical.computeOffset(center, radius, degreeStep * i);
points.push([gpos.lng(), gpos.lat()]);
};
// Duplicate the last point to close the geojson ring
points.push(points[0]);
return {
type: 'Polygon',
coordinates: [ points ]
};
}
The geometry library is not included by default. You must specifically ask for it via the libraries parameter.

D3 + Leaflet: d3.geo.path() resampling

We've adapted Mike Bostock's original D3 + Leaflet example:
http://bost.ocks.org/mike/leaflet/
so that it does not redraw all paths on each zoom in Leaflet.
Our code is here: https://github.com/madeincluj/Leaflet.D3/blob/master/js/leaflet.d3.js
Specifically, the projection from geographical coordinates to pixels happens here:
https://github.com/madeincluj/Leaflet.D3/blob/master/js/leaflet.d3.js#L30-L35
We draw the SVG paths on the first load, then simply scale/translate the SVG to match the map.
This works very well, except for one issue: D3's path resampling, which looks great at the first zoom level, but looks progressively more broken once you start zooming in.
Is there a way to disable the resampling?
As to why we're doing this: We want to draw a lot of shapes (thousands) and redrawing them all on each zoom is impractical.
Edit
After some digging, seems that resampling happens here:
function d3_geo_pathProjectStream(project) {
var resample = d3_geo_resample(function(x, y) {
return project([ x * d3_degrees, y * d3_degrees ]);
});
return function(stream) {
return d3_geo_projectionRadians(resample(stream));
};
}
Is there a way to skip the resampling step?
Edit 2
What a red herring! We had switched back and forth between sending a raw function to d3.geo.path().projection and a d3.geo.transform object, to no avail.
But in fact the problem is with leaflet's latLngToLayerPoint, which (obviously!) rounds point.x & point.y to integers. Which means that the more zoomed out you are when you initialize the SVG rendering, the more precision you will lose.
The solution is to use a custom function like this:
function latLngToPoint(latlng) {
return map.project(latlng)._subtract(map.getPixelOrigin());
};
var t = d3.geo.transform({
point: function(x, y) {
var point = latLngToPoint(new L.LatLng(y, x));
return this.stream.point(point.x, point.y);
}
});
this.path = d3.geo.path().projection(t);
It's similar to leaflet's own latLngToLayerPoint, but without the rounding. (Note that map.getPixelOrigin() is rounded as well, so probably you'll need to rewrite it)
You learn something every day, don't you.
Coincidentally, I updated the tutorial recently to use the new d3.geo.transform feature, which makes it easy to implement a custom geometric transform. In this case the transform uses Leaflet’s built-in projection without any of D3’s advanced cartographic features, thus disabling adaptive resampling.
The new implementation looks like this:
var transform = d3.geo.transform({point: projectPoint}),
path = d3.geo.path().projection(transform);
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
As before, you can continue to pass a raw projection function to d3.geo.path, but you’ll get adaptive resampling and antimeridian cutting automatically. So to disable those features, you need to define a custom projection, and d3.geo.transform is an easy way to do this for simple point-based transformations.

Erratic overlay behavior with custom projection, Google Maps API v3

I want to browse a single image with the Google Maps API, for which I've defined my own projection. I wanted to use a GroundOverlay instead of several image tiles, because I only have one small-resolution image, but I wanted it to still be zoomable. However, I get some erratic behavior when trying to work with this projection:
No overlays show up at all at zoom level 0.
At zoom level 1 and higher, Markers show up, but GroundOverlays still don't.
However, I can get GroundOverlays to show up very briefly, if I zoom out from any level. It will only show while it's zooming out and disappear again immediately. Also, while it does show up shortly, it does not show up at the right coordinates, but the Markers do.
I'm rather new to the API, so I would not be surprised if it was a simple oversight on my part, but I just can't see what could cause this. Here is the code for my projection, which just maps the lat/lng linearly to map coordinates:
function EvenMapProjection() {
var xPerLng = 512/360;
var yPerLat = 512/180;
this.fromLatLngToPoint = function(latlng) {
var x = (latlng.lng()+180)*xPerLng;
var y = (latlng.lat()+90)*yPerLat;
console.log('Lng', latlng.lng(), 'Lat', latlng.lat(), '-> Point', x, y);
return new google.maps.Point(x, y);
};
this.fromPointToLatLng = function(point) {
var lat = point.y/yPerLat-90;
var lng = point.x/xPerLng-180;
console.log('Point', point.x, point.y, '-> Lng', lng, lat);
return new google.maps.LatLng(lat, lng);
};
}
An example of what I'm trying to do without the projection (using the default Mercator projection):
http://95.156.209.71/tmp/a.html
The same example with the projection as defined above:
http://95.156.209.71/tmp/b.html
And finally an example using the projection but without the GroundOverlay, and instead just using tiled images (always the same image):
http://95.156.209.71/tmp/c.html
The last link also shows the Marker at LatLng(0, 0) appear at zoom level 1 (or higher), but not at level 0.
Is there something I'm just missing, or some buggy code, or is this actually a problem in the API?
I just found out that my mistake was in the definition of the ground overlay. I was at zoom level 0, which meant that I set the bounds for the overlay from (-90,-180) to (90,180), but the API seems to have issues with these levels, because they wrap longitude, hence I got weird errors. I adjusted it to be at level 1 for minimum zoom, and set the overlay from (-45,-90) to (45,90), and now it all works fine.

Categories