Custom CRS: How to get the leaflet scale control to display mm (millimeter) and um (micrometer) - leaflet

Is there anyway to get leaflet to display values smaller than a meter? For instance, mm (millimeter), or um (micrometer) on the scale control?
Or if there is a plugin that does this?
I have a custom map with a custom CRS that uses virtual microscopy images.
I use the following code to create a map with values less than a meter however the scale control is really wide and doesn't go below a meter:
L.CRS.Meters = L.extend(L.CRS, {
projection: L.extend( L.Projection.LonLat, {
bounds: L.bounds([0, 0], [2160, 4096])
}),
transformation: new L.Transformation(1, 0, -1, 0),
scale: function (zoom) {
return Math.pow(2, zoom);
},
infinite: false
});
var customCRS = L.extend(L.CRS.Simple, {
projection: L.extend( L.Projection.LonLat, {
bounds: L.bounds([0, 0], [2160, 4096])
}),
transformation: new L.Transformation(1, 0, 1, 0),
scale: function (zoom) {
return Math.pow(2, zoom +7);
},
infinite: false
});
var map = L.map('vm', { zoomSnap: 0.2, crs: customCRS}).setView([3, 3], 3);

The specific piece of code that handles the rounding of the scale bar measurement and the (metric) units is the _updateMetric method:
_updateMetric: function (maxMeters) {
var meters = this._getRoundNum(maxMeters),
label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km';
this._updateScale(this._mScale, label, meters / maxMeters);
},
Note that the implementation of this._getRoundNum() will return an integer number, i.e. 1 minimum.
You might want to replace the implementation of _updateMetric(), to round up a factor of that number, and apply unit suffixes accordingly, e.g.:
L.Control.Scale.include({
_updateMetric: function(maxMeters) {
var maxMilliMeters = maxMeters * 1000,
milliMeters = this._getRoundNum(maxMilliMeters),
label = milliMeters < 1000 ? milliMeters + " mm" : milliMeters / 1000 + " m";
console.log(this._mScale, label, milliMeters / maxMilliMeters);
this._updateScale(this._mScale, label, milliMeters / maxMilliMeters);
}
});
See a working example here.

Related

Get pixel coordinates of point in stacked series

In order to make my own markPoint div (since I cannot get Echarts markPoints to show up), I need to get the x,y pixel coordinates of a visual point on a stacked line chart where one series hits a 0 value. But because it is stacked, that 0 point isn't at the bottom of the chart, but somewhere higher sitting on top of other series lower in the stacking order. the echartsInstance.convertToPixel function is responding back with the y pixel coordinate at the bottom of the chart, where absolute 0 would be, but that's not where the series is. It's higher up, even with a 0 value, due to stacking.
Is there anyway, maybe through getModel() or getZr() to find out what the pixel coordinates are for a series (where it actually appears visually after stacking) at a given set of its own x,y values?
OK, let's imagine that you have a bar chart and you want to place the markPoint on the top of second bar, drawing the chart:
var myChart = echarts.init(document.getElementById('main'));
var option = {
xAxis: {
data: ["A", "B", "C"]
},
yAxis: {},
series: [{
name: 'Series1',
type: 'bar',
data: [5, 20, 36],
}]
}
myChart.setOption(option);
Then you should define the markPoint in series config:
// ...
markPoint: {
symbol: 'circle',
symbolSize: 10,
itemStyle: { color: 'black' },
data:[{
// here you defined: x = 'B' and y = '20'
coord: ['B', 20]
}]
}
// ...
Here you can find the full example.
Next. You want to place the div point with pixel coordinates depending on the size of bar. Here you convert the pixel coordinates into Echarts scale:
var [chartX, chartY] = myChart.convertToPixel({ seriesIndex: 0 }, [x, y]);
You need function to draw point on page:
function Point(opts){
var { x,y } = opts;
var canvas = document.getElementById('main');
var point = document.createElement('div');
point.classList.add('point');
var [chartX, chartY] = myChart.convertToPixel({ seriesIndex: 0 }, [x, y]);
point.style.left = chartX + 'px';
point.style.top = chartY + 'px';
return canvas.insertAdjacentElement('afterbegin', point);
}
and then call it:
var p1 = new Point({ x: 'B', y: 20 });
It's all. See full example.

Calculate lat long on static mapbox img for 256px tiles

I have a static image with only center point lat/long (for example https://api.mapbox.com/styles/v1/mapbox/light-v9/static/-78.4649,42.5128,5,0,0/300x200) and I want to put on this map some markers(lat.long) with the help of canvas.
But I need to calculate somehow the xy coordinates for those markers.
So I know the center of map(lat/long) and the lat/long marker coordinates. Is there any way to convert lat/long to xy knowing only zoom level and center?
Or if I know the xy of the center lat/long(it always be the same 150px * 100px) and zoom level, could I calculate the xy for other markers?
I have a lot of markers (>200, and they all are custom svg generated and so on) to place it on this map. I can't use mapbox mapbox static map because of the markers limitation and so on.
UPD: Based on the comments I updated the question.
How to calculate it for 256px square tiles?
Based on the OP comment I'm assuming that the requested image is square, for the sake of simplicity (TILE_SIZE could be decomposed in a TILE_SIZE_X and TILE_SIZE_Y component). I'm also assuming that the image is 256-pixels wide TILE_SIZE=256
I'm giving both the pixel coordinates relative to the center of the image (distanceInPixels function), and to the Lower Left Corner (imageCoordinates function). Changing to the Upper Left Corner in case that's necessary should be trivial (X will be equal and Y = TILE_SIZE -Y).
<!DOCTYPE html>
<html>
<body>
<p id="demo"></p>
<script>
var latLngMarker = {};
var latLngCenter = {};
// Image dimensions in pixels
var TILE_SIZE = 256;
var zoom = 5;
// Coordinates of the marker to be projected on the image
latLngMarker.lat = 41.850;
latLngMarker.lng = -87.650;
// Coordinates of the image center
latLngCenter.lat = 41.850;
latLngCenter.lng = -87.650;
// Coordinates projected on the cartographic plane (Mercator)
var centerProjected = project(latLngCenter);
var markerProjected = project(latLngMarker);
// The result should be X=Y=0, because I made Marker Lat Lng = Center Lat Lng
var distanceFromCenter = distanceInPixels(centerProjected, markerProjected);
alert("X: " + distanceFromCenter.x + " Y: " + distanceFromCenter.y);
// The result should be X=Y=256/2=128 for the same reason
var coords = imageCoordinates(centerProjected, markerProjected);
alert("X: " + coords.x + " Y: " + coords.y);
// The horizontal distance represented by one pixel for a given latitude and zoom level
function pixelResolution (latLng, zoom) {
var radius = 6378137.0 // semi-axis of WGS84 ellipsoid
var circumference = 2 * Math.PI * radius;
var distancePerImage = circumference * Math.cos(latLng.lat * Math.PI / 180.0) / Math.pow(2,zoom);
var distancePerPixel = distancePerImage / TILE_SIZE;
return distancePerPixel
}
// Web mercator projection.
function project(latLng) {
var siny = Math.sin(latLng.lat * Math.PI / 180);
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
var xy = {};
xy.x = TILE_SIZE * (0.5 + latLng.lng / 360);
xy.y = TILE_SIZE * (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
return xy
}
// Marker pixel coordinates relative to the image Center
function distanceInPixels(centerProjected, markerProjected) {
var delta = {};
var spacing = pixelResolution(latLngCenter, zoom);
delta.x = Math.round((centerProjected.x - markerProjected.x)/spacing);
delta.y = Math.round((centerProjected.y - markerProjected.y)/spacing);
return delta
}
// Marker pixel coordinates relative to the Lower Left Corner
function imageCoordinates(centerProjected, markerProjected) {
var pixelCoordinates = {};
var spacing = pixelResolution(latLngCenter, zoom);
var deltaPixels = distanceInPixels(centerProjected, markerProjected);
pixelCoordinates.x = TILE_SIZE / 2 - deltaPixels.x;
pixelCoordinates.y = TILE_SIZE / 2 - deltaPixels.y;
return pixelCoordinates
}
</script>
</body>
</html>
Note: I can confirm that the pixelResolution function only works with square image tiles with dimensions of powers of 2. The Math.pow(2,zoom); snippet gives the game away!
Web Mercator function based on:
https://developers-dot-devsite-v2-prod.appspot.com/maps/documentation/javascript/examples/map-coordinates
Horizontal distance represented by one pixel from :
https://wiki.openstreetmap.org/wiki/Zoom_levels
See also:
https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
If you're going to linearly interpolate you'd need to know the lat/long & x/y for 2 points. It wouldn't be possible with only the center point unless you also have a conversion metric for pixels - ie. 50 pixels is .1 delta lat/long.
If you have the lat/long & x/y for two points you can create the ratio as y1 - y2 / lat1-lat2 or x1-x2/long1-long2 each of which should result in the same ratio
Then it'd be relatively easy, assume the ratio is 5 meaning 5px/l so you had a point that was (3,-4) away from that center point you'd simply multiple to find the pixel offset (15,-20) and add that to the center = (165, 80).
Since all of your images are zoomed the same amount you could manually calculate the ratio once and store it as a constant.
sudo/untested python:
def getRatio(latlongs=[(1,1),(0,0)], xys=[(5,5),(0,0)]:
return (xys[0][1]-xys[1][1]) / (latlongs[0][0] - latlongs[1][0])
centerLatLong = (5,5)
centerXY = (150, 100)
def getCoord(lat,long,ratio):
y = (lat-centerLatLong[0])*ratio + centerXY[1]
x = (long-centerLatLong[1])*ratio + centerXY[0]
return x, y

Convert coordinates between two rotated systems

I have a map with coordinates in meters and an overlaying building plan with pixel coordinates:
I already know the scale factor and I am able to convert coordinates between the two systems if they are aligned (i.e. the overlay image is exactly horizontal with no rotation)--> conversionfactor (= number of overlay pixels in one meter on the map)
MAPx(ImageX) = centerpointX + ImageX * conversionfactor
MAPy(ImageY) = centerpointY + ImageY * conversionfactor
How can I convert between the coordinates if the overlay is rotated assuming that I have above formulas and I want to include a rotation angle?
EDIT (#tsauerwein):
Here is the marker style that you have requested:
planStyle = function(feature, resolution){
var style = new ol.style.Style({
image: new ol.style.Icon({
src: feature.dataURL,
scale: feature.resolution / resolution,
rotateWithView: true,
rotation: feature.rotation * (Math.PI / 180),
anchor: [.5, .5],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
opacity: feature.opacity
})
})
return [style];
};
Assuming that you are using ol.source.ImageStatic: When you configure your layer, you have the size of the image in pixels(e.g. width=500, height=200) and also the extent that this image covers in coordinates.
Now, if you have a coordinate, you can easily check if the coordinate is inside the image extent (ol.extent.containsXY(extent, x, y)). Then you can also translate the real-world coordinate to a pixel coordinate:
// image size
var width = 500;
var height = 250;
// image extent
var extent = [2000, 0, 4000, 1000];
// coordinates
var x = 3000;
var y = 500;
if (ol.extent.containsXY(extent, x, y)) {
var pixelX = width * (x - extent[0]) / ol.extent.getWidth(extent);
var pixelY = height * (y - extent[1]) / ol.extent.getHeight(extent);
}
Doing it like this, it doesn't matter if the map is rotated or not.

Find circles which the user is in, according to each circle's relative radius

Here's the pitch. I have a collection of Circles which have mainly two attributes: location is a Point and radius is a distance in meters.
Users also have a profile.location Point attribute.
In my publications, I want to find all the Circles that the user is "in", ergo the ones he or she is near enough according to each Circle's radius attribute. To sum it up, here's how it would look like:
Meteor.publish('circles', function() {
var curUser = Meteor.users.findOne({_id:this.userId});
if (curUser) {
return Circles.find({
location: {$near:
{$geometry:curUser.profile.location,$maxDistance:self.radius} // HERE
}
}));
}
this.ready();
});
Except self.radius is a completely made-up term on my behalf. But is it possible to achieve something like this?
POST-SOLVING edit:
Thanks to Electric Jesus, I have my matchings working perfectly with polygons, since circles are not GeoJSON types as of yet. (therefore they are not single attributes that can be queried, sort of) So I converted my circles into polygons! Here is a JavaScript function to do this:
function generateGeoJSONCircle(center, radius, numSides) {
var points = [];
var earthRadius = 6371;
var halfsides = numSides / 2;
//angular distance covered on earth's surface
var d = parseFloat(radius / 1000.) / earthRadius;
var lat = (center.coordinates[1] * Math.PI) / 180;
var lon = (center.coordinates[0] * Math.PI) / 180;
for(var i = 0; i < numSides; i++) {
var gpos = {};
var bearing = i * Math.PI / halfsides; //rad
gpos.latitude = Math.asin(Math.sin(lat) * Math.cos(d) + Math.cos(lat) * Math.sin(d) * Math.cos(bearing));
gpos.longitude = ((lon + Math.atan2(Math.sin(bearing) * Math.sin(d) * Math.cos(lat), Math.cos(d) - Math.sin(lat) * Math.sin(gpos.latitude))) * 180) / Math.PI;
gpos.latitude = (gpos.latitude * 180) / Math.PI;
points.push([gpos.longitude, gpos.latitude]);
};
points.push(points[0]);
return {
type: 'Polygon',
coordinates: [ points ]
};
}
Here you go. I don't really know how many sides I should use, so I left an argument for that too. From there, you can use Electric Jesus's answer to get where I was going. Don't forget to put a 2dsphere index on your polygon!
Circles._ensureIndex({'polygonConversion': "2dsphere"});
No, the Geo indexes would never work in a way that you demand it to be dynamic according to a document's 'radius'. You must convert your circles into Polygon geometries and use the $geoIntersects query to find which Circle (Polygon geometry) intersects with your current location/location parameter (Point geometry).
var location = {
longitude: -1.85549,
latitude: 52.9445
}
// create a circle with a Polygon geometry location, with converted coordinates
Circles.insert({name: "My Circle 1", loc: {
type: "Polygon",
coordinates: [[[ ... ]]],
}
});
// finding one or more Circles that intersect with current location.
Circles.find({loc: {
$geoIntersects: {
$geometry: {
type: "Point" coordinates: [location.longitude, location.latitude]
}
}
}});
Mongo's geospatial operators include $centerSphere, which returns documents that are within the bounds of a circle:
Entities.find({
location : {
$geoWithin : {
$centerSphere: [
[ curUser.profile.location.lng , curUser.profile.location.lat ],
radius / 6378100 // convert radians to meters by dividing by the Earth's radius
]
}
}
} );
You can try an additively weighted voronoi diagram. The distance function is the euklidian distance minus the weight. Sites with bigger weights and nearby other sites get sorted into the same cell.

Generate random GeoJSON polygons

Is there a way or tool to generate random GeoJSON polygons of specific size withing bounding box? Specifically I want to populate mongodb with a lot of random polygons and test specific functionality.
You could do it programmatically using the bounding box coordinates to generate the random bounding box coords for the rectangles.
For example, if your bounding box is [[100,100],[200,200]] you could do the following:
// generate a random width and height
// (e.g. with random numbers between 1 and 50)
var width = Math.floor(Math.random() * 50) + 1;
var height = Math.floor(Math.random() * 50) + 1;
// generate a random position that allows the rectangle to fit within the bounding box walls
// 100 is used in the calculation as 100 is the width and height of the example bounding box
var upperX = Math.floor(Math.random() * (100-width)) + 1;
var upperY = Math.floor(Math.random() * (100-height)) + 1;
var lowerX = upperX + width;
var lowerY = upperY + height;
var bounds = [[upperX, upperY], [lowerX, lowerY]];
// create rectangle
L.rectangle(bounds, {color: "#ff7800", weight: 1}).addTo(map);
// loop through above code some chosen number of times