Use static image as a world map in java-script - javascript

I am trying to render an image like below and want it to act like the world map.
I want to create a javascript function which shows location dynamically into the image when I pass (latitude, longitude) pair.
I have tried google map API documented in "developers.google.com/maps/documentation/javascript/examples/…". But the problem is that image is not in that projection type. So results are so different than expected.

Your image appears to use a plate carreé projection, which is commonly used for raster data sets of the world. This projection easily allows reprojection - and as rioV8 suggests, simply maps longitude and latitude as though they were x,y coordinates on a Cartesian plane rather than a spheroid.
The image you have shared is twice as wide as high, which also helps confirm a plate carreé: if the projection maps longitude and latitude as Cartesian coordinates the image should be twice as wide as high (360 degrees around, 180 degrees pole to pole).
Now we need to make a projection for this (not particularly difficult) or we could use a d3 projection: d3.geoEquirectangular() (plate carreé goes by a few names).
I'll just go over the d3 method here, if using the d3 projection, we need to understand the scale of the projection. The default value takes 2π radians and maps it to 960 pixels, which gives us a scale of: 960/2π. If we want to wrap 2π radians to an arbitrary image width we use a scale of width/2π.
So we could project points relative to this image with:
d3.geoEquirectangular()
.scale(width/Math.PI/2)
.translate([width/2,height/2]) // image width/height so [0,0] is centered
var width =400;
var height = 200;
var projection = d3.geoEquirectangular()
.scale(width/Math.PI/2)
.translate([width/2,height/2]);
var svg = d3.select("svg");
svg.append("circle")
.attr("r", 4)
.attr("fill","yellow")
.attr("transform", "translate("+projection([79.842778,6.934444])+")")
img, svg {
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div>
<img src="https://i.stack.imgur.com/4p3MK.jpg" width="400" height="200"/>
<svg width="1958" height="929"></svg>
</div>
Of course once we know the projection, we could reproject the original raster image (eg.), and project points onto that.

Related

Looking for a world map with a specific projection in d3js

I have played around with the d3js (v5) maps,
i'm trying to generate this map (the screenshot was taken from a random website),
For my particular case there is no need to present Antarctica.
I have read the documentation here: https://github.com/d3/d3-geo#projections,
and followed the instructions and used geoMercator, got this flat map which gets cutoff in the top north for some reason.
What is the correct approach for getting the first map's layout?
any suggestions?
The projection you are looking at is a Mercator projection.
With d3.geoMercator(), the scale value is derived from the circumference of the cylinder that forms the projection surface. The scale value is the number of pixels per radian. The default value anticipates stretching the 360 degrees of the cylinder over 960 pixels: 960/Math.PI/2.
For vertical angular distances, there is no such scaling factor, as one moves to extreme longitudes, the angular distance between points is increasingly exaggerated, such that the poles will be at ± infinity on the y axis. Because of this Mercator's, especially web Mercator's are often truncated at ±~85 degrees. With an extent of [-180,85] and [180,-85], a Mercator is square.
This limit is incorporated into d3-geoMercator, which "Defines a default projection.clipExtent such that the world is projected to a square, clipped to approximately ±85° latitude. (docs)"
This means that if we want to show the full extent of a d3-geoMercator, across 960 x 960 pixels, we can use:
d3.geoMercator()
.scale(960/Math.PI/2) // 960 pixels over 2 π radians
.translate([480,480]) // the center of the SVG/canvas
Which gives us:
The default center of d3-geoMercator is [0°,0°], so if we want [0°,0°] to be in the middle of the SVG/canvas, we translate the center so that it is in the middle, with a translate of [width/2,height/2]
Now that we are showing the whole world, we can refine to show only the portion we want. The simplest method might just be lopping off pixels from the bottom of the svg/canvas. Using the above code with a canvas/svg height of 700 pixels (and keeping 960 pixels across, using the same scale and translate) I get:
I did not remove Antarctica from this image - it just happens that it is cut off without having to filter it out (this is not necessarily ideal practice: it is still drawn).
So, an SVG/Canvas with width 960, height 700, with a projection scale of 960/Math.PI/2 and a translate of [480,480] appears to be ok. These values will scale together for different view port sizes.
With maps, there is often a lot of eyeballing to get the visual effect desired, tweaking projection.translate() or projection.center() can help shift the map to the desired location. But we can do this computationally. I'll speak to one method here, using projection.fitSize() (though this won't solve the required aspect ratio without extra steps).
Project.fitSize([width,height],geojson) takes an array specifying the dimensions of the SVG/canvas and a geojson object and tweaks the projection scale and translate values so that the geojson feature is contained in the SVG/canvas. The geojson feature could be a bounding box of the part of the world you want to show, so you could use:
projection.fitSize([width,height], {
type: "Polygon",
coordinates: [[
[-179.999,84] ,
[-179.999,-57] ,
[179.999,-57] ,
[179.999,84],
[-179.999,84]
]]
})
Where ~84 degrees north is the north end of Greenland and ~56 degrees south is roughly the tip of South America. This will ensure that the entire portion of the world you want to see is visible. However, as noted above, this doesn't consider aspect, so if you constrain the above extent to square dimensions, you'll still be showing the full extent of the Mercator.

Mapbox.js: set map bounds on an area on both side of the date-line

In Mapbox, the fitBounds method doesn't seem to play nicely with points that are on both sides of the Atlantic (where the coords jump from 359 to 0).
How is it possible fit the bounds of Alaska or a flight from HK to SF. It might be possible to code a workaround making use of a center point and a custom zoom-level but it feel like a overengineered workaround around a poor handling of the bounds.
If firBounds is no help, is there another method like that would achieve similar results?
Example of fitBounds around Alaska, since Alaska crosses the antimeridian:
map.fitBounds([[172.461667,71.365162],[-129.979511,51.214183]])
Those cases are always a bit of a pain. What may help is wrapping your bounding box coordinates using:
https://www.mapbox.com/mapbox-gl-js/api/#lnglat#wrap
Another solution might be to convert the bounds to a center & zoom value using:
https://github.com/mapbox/geo-viewport
const {width, height} = map.getCanvas().getBoundingClientRect();
const viewport = geoViewport(
[minX, minY, maxX, maxY], // the bounding box (west, south, east north)
[width, height], // map dimensions in pixel
0, // minzoom
22, // maxzoom
512 // tilesize for mapbox-gl-js
);
map.setCenter(viewport.center);
map.setZoom(viewport.zoom)

How to reduce visible part (or mask) of mapbox style layer?

I have hexagonal tileset added as a layer to mapbox style though mapbox studio.
Trying to find possibility to reduce layer visible area. For example to show only 100m radius (or square with side equal to 100m) area around map center (current point marker).
Is this possible?
You can create a bounding box and use fitBounds method of the map, for example:
const boundingBox = [
[minX, minY],
[maxX, maxY]
];
map.fitBounds(boundingBox);
More about fitBounds and other examples you can find here.
For creating bounding box you can use Turf.js library.
This code uses buffer and bbox methods to create bounding box with 100m side and given point in the center:
const pointBuffer = turf.buffer(point /* - your point GeoJSON */, 0.1, 'kilometers');
const boundingBox = turf.bbox(pointBuffer);

Setting projection of d3.geoPath() not working as documented

I've made a lot of maps in D3 and never had this problem, where the output is so small you can't see it. I've included a small geojson file, which is easily viewable using various other tools (e.g. here).
It appears that the projection is not being set properly with d3.geoPath(). The API specifies that d3.geoPath() takes a projection argument, however when I try to set the projection using this method (as in the example below), the resulting map is not projected properly. It's only after I set the projection using the d3.geoPath().projection() method that I obtain the proper result.
Is this an error in the documentation or am I missing something?
//setup and map parameters
var height = 300;
var width = 300;
var projection = d3.geoAlbers().scale(1000).translate([width/2, height/2]);
var path = d3.geoPath(projection);
//geoJSON data
var geojson =
{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-86.910592,33.536105],[-86.907192,33.542204999999996],[-86.90539199999999,33.541305],[-86.901608,33.543456],[-86.895692,33.548805],[-86.89249199999999,33.550404],[-86.888392,33.549304],[-86.879772,33.553267999999996],[-86.868549,33.553418],[-86.8651,33.557134999999995],[-86.856129,33.562064],[-86.85159499999999,33.559404],[-86.848891,33.559805],[-86.852991,33.552104],[-86.85519099999999,33.543904999999995],[-86.855694,33.538151],[-86.860817,33.53405],[-86.850509,33.535693],[-86.83124699999999,33.544767],[-86.827029,33.534708],[-86.82383999999999,33.533389],[-86.826703,33.528081],[-86.826807,33.521107],[-86.822808,33.514319],[-86.81474899999999,33.502386],[-86.81411899999999,33.500078],[-86.816041,33.496234],[-86.823825,33.488839],[-86.825743,33.485408],[-86.82772299999999,33.477393],[-86.828558,33.472626999999996],[-86.82791,33.46967],[-86.833503,33.467552999999995],[-86.839366,33.463845],[-86.841534,33.460941],[-86.842309,33.465699],[-86.849758,33.466302],[-86.861663,33.456759],[-86.869367,33.4514],[-86.87749699999999,33.449382],[-86.884293,33.445453],[-86.88948599999999,33.441072999999996],[-86.890131,33.438437],[-86.899491,33.434508],[-86.908391,33.429308],[-86.922929,33.415512],[-86.925484,33.419897999999996],[-86.93114299999999,33.422801],[-86.93749199999999,33.421608],[-86.945352,33.425982999999995],[-86.946468,33.428407],[-86.937809,33.435314],[-86.925764,33.449968999999996],[-86.920859,33.454651999999996],[-86.92215,33.456643],[-86.927679,33.453849999999996],[-86.932965,33.449332],[-86.935435,33.446132],[-86.94133699999999,33.442150999999996],[-86.941301,33.445651999999995],[-86.942842,33.450103],[-86.946817,33.45017],[-86.95131599999999,33.45181],[-86.950279,33.458104],[-86.945494,33.463234],[-86.929693,33.478538],[-86.92935299999999,33.479858],[-86.920255,33.492874],[-86.92450699999999,33.493904],[-86.93110999999999,33.496843],[-86.933543,33.494935999999996],[-86.94144999999999,33.494743],[-86.944717,33.495934],[-86.946237,33.502279],[-86.946224,33.506071],[-86.941811,33.507621],[-86.937693,33.512206],[-86.92634,33.512381999999995],[-86.917255,33.517139],[-86.915435,33.519832],[-86.908779,33.518958999999995],[-86.903385,33.516537],[-86.898659,33.520165999999996],[-86.894459,33.526136],[-86.896992,33.530504],[-86.910592,33.536105]]]},"properties":{"a":1}}
]}
var svg = d3.select("#map");
svg.selectAll("path").data(geojson.features).enter().append("path").attr("fill","red").attr("d", path);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg height="300px" width="300px" id="map"> </svg>
You will need to set your projection parameters correctly. A scale factor of 1 000 is much to small to show a small area as in your example. A scale factor of 50 000 might be more appropriate. You also need to properly center your map.
You will need to find a point near the center of your feature to properly center the map. You can do this manually by using google earth, or other methods to find a path centroid in geographic coordinates (not svg coordinates; however, you can find the svg centroid to refine the projection as well by translating features, I am not using that approach in my answer though).
With an Albers projection you should center on the y axis and rotate on the x axis:
var projection = d3.geoAlbers()
.rotate([-x,0])
.center([0,y])
.scale(k)
.translate([width/2,height/2])
You want to use the negative of your central meridian because you rotate the globe under the map. See this answer for a more graphical explanation of an Albers parameters
An Albers projection in d3 has default center and rotate coordinates. If you don't set the center coordinates with both rotate and center, the map will keep default parameters that are intended to show the entire US; this will modify the intended projection. Other projections are generally centered on [0,0] in d3 by default, which is off the coast of Africa.
I eyeballed a center coordinate using google earth to use in this projection. The centering point I am using here is:
86.884 degrees West (-86.884 degrees East)
33.507 degress North
I also zoomed in much further than your original zoom factor, using 100 000 rather than 1 000. Here is the projection showing your data:
//setup and map parameters
var height = 300;
var width = 300;
var projection = d3.geoAlbers()
.scale(100000)
.translate([width/2, height/2])
.rotate([86.884,0])
.center([0,33.507]);
var path = d3.geoPath(projection);
//geoJSON data
var geojson =
{"type":"FeatureCollection","features":[
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-86.910592,33.536105],[-86.907192,33.542204999999996],[-86.90539199999999,33.541305],[-86.901608,33.543456],[-86.895692,33.548805],[-86.89249199999999,33.550404],[-86.888392,33.549304],[-86.879772,33.553267999999996],[-86.868549,33.553418],[-86.8651,33.557134999999995],[-86.856129,33.562064],[-86.85159499999999,33.559404],[-86.848891,33.559805],[-86.852991,33.552104],[-86.85519099999999,33.543904999999995],[-86.855694,33.538151],[-86.860817,33.53405],[-86.850509,33.535693],[-86.83124699999999,33.544767],[-86.827029,33.534708],[-86.82383999999999,33.533389],[-86.826703,33.528081],[-86.826807,33.521107],[-86.822808,33.514319],[-86.81474899999999,33.502386],[-86.81411899999999,33.500078],[-86.816041,33.496234],[-86.823825,33.488839],[-86.825743,33.485408],[-86.82772299999999,33.477393],[-86.828558,33.472626999999996],[-86.82791,33.46967],[-86.833503,33.467552999999995],[-86.839366,33.463845],[-86.841534,33.460941],[-86.842309,33.465699],[-86.849758,33.466302],[-86.861663,33.456759],[-86.869367,33.4514],[-86.87749699999999,33.449382],[-86.884293,33.445453],[-86.88948599999999,33.441072999999996],[-86.890131,33.438437],[-86.899491,33.434508],[-86.908391,33.429308],[-86.922929,33.415512],[-86.925484,33.419897999999996],[-86.93114299999999,33.422801],[-86.93749199999999,33.421608],[-86.945352,33.425982999999995],[-86.946468,33.428407],[-86.937809,33.435314],[-86.925764,33.449968999999996],[-86.920859,33.454651999999996],[-86.92215,33.456643],[-86.927679,33.453849999999996],[-86.932965,33.449332],[-86.935435,33.446132],[-86.94133699999999,33.442150999999996],[-86.941301,33.445651999999995],[-86.942842,33.450103],[-86.946817,33.45017],[-86.95131599999999,33.45181],[-86.950279,33.458104],[-86.945494,33.463234],[-86.929693,33.478538],[-86.92935299999999,33.479858],[-86.920255,33.492874],[-86.92450699999999,33.493904],[-86.93110999999999,33.496843],[-86.933543,33.494935999999996],[-86.94144999999999,33.494743],[-86.944717,33.495934],[-86.946237,33.502279],[-86.946224,33.506071],[-86.941811,33.507621],[-86.937693,33.512206],[-86.92634,33.512381999999995],[-86.917255,33.517139],[-86.915435,33.519832],[-86.908779,33.518958999999995],[-86.903385,33.516537],[-86.898659,33.520165999999996],[-86.894459,33.526136],[-86.896992,33.530504],[-86.910592,33.536105]]]},"properties":{"a":1}}
]}
var svg = d3.select("#map");
svg.selectAll("path").data(geojson.features).enter().append("path").attr("fill","red").attr("d", path);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg height="300px" width="300px" id="map"> </svg>

leaflet fixed size circle on map

I am using leaflet.js to create few markers and circles. I am using the below given code to draw circles : -
L.circle([ lat, lng ], 1000, {
color : colorCode,
stroke : false,
fillColor : colorCode,
fillOpacity : 0.7
});
Now if I edit this circle on UI and drag this circle vertically downwards, the circle size increases and vice a versa. Similar issue is with calling the above given method with different lat lngs. The same radius (1000) sized circle get plotted with different sizes on map.
My requirement is to place marker with same radius with same size on map everywhere.
I checked L.circleMarker but it takes radius in pixels and also circleMarkers does not scale in zoomin zoomout events. That is why I can't use circleMarkers.
I changed the crs option to 4326 but no success. I am using imageOverlay not tileset. I have created a fiddle.
http://jsfiddle.net/newBee_/88bdrzkr/12/
Try creating a circle on top area then edit and move it downwards. It's size increases. This is what I want to stop. This will resolve the problem of generating circle of same radius via code in different area of map with same size. Please help.
Please suggest.
Edit:
It looks like this is a bug deep into Leaflet 0.x: L.Circle radius computation uses hard-coded Earth projection rather than the specified CRS. Leaflet 1.0 seems to correctly check for the CRS before using the Earth-related computation.
For your case, simply overriding the faulty method seems to fix it, at least visually.
Demo: http://jsfiddle.net/88bdrzkr/13/
The "corrected" method to include in your script:
L.Circle.include({
_getLngRadius: function () {
return this._getLatRadius();
}
});
Regarding iH8's answer, the trick to override L.CRS.Simple.scale is similar to highly zooming (the 256 factor expands the latLng to much further pixels - any high number will do). At high zoom, you are moving your circle along a very short distance, for which the latitude does not change much. So you do not see any visible difference in radius, even though the bug is still there.
Demo of using just higher zoom, no method override at all: http://jsfiddle.net/kau6g8fk/1/
For your need where the circle looks to be more like a visual aid, any of these 3 solutions is enough.
Edit: the CRS is not the issue at all.
Previous message:
If you use Leaflet for indoor mapping, as your jsFiddle suggests (or any flat type map, as opposed to the projection of a sphere like Earth on to a plane), you could simply use L.CRS.Simple
Striked out this faulty solution as pointed out by Ghybs in his answer
Very weird issue, turns out that overloading L.CRS.Simple's scale method to return 256 * Math.pow(2, zoom) fixes this. Here's a fork of your JSFiddle: http://jsfiddle.net/kau6g8fk/ I'm unsure as to the cause of this issue, it would require more research. Will do if i find the time. Found the solution here: http://codepen.io/mike_beweb/pen/BymKGe
The answer below was given before the poster edited his/her question and showed that the used CRS was L.CRS.Simple while i presumed the default CRS. I'll leave it in tact because it might come in handy for some users:
The size change on drag of your L.Circle's is because of your map's default spherical mercator projection (EPSG:3857). Best explained with an image, here's a map with a graticule overlay on every 10 degrees:
Demo on Plunker: Leaflet 0.7.5 EPSG:3857 Spherical
As you move further from the equator every plane becomes higher. Thus your circle automaticly becomes higher the further north/south you drag it. You could use a equirectangular projection (EPSG:4326), in which every plane has the same size regardless of the distance from the equator:
Demo on Plunker: Leaflet 0.7.5 EPSG:4326 Equirectangular
With equirectangle projection you won't have the problem you're having now but you'll have to change your tileset to one with EPSG:4326 projection and those are hard to come by compared to EPSG:3857 tilesets.
If you're not willing or unable to change projection another solution could be to hack around L.CircleMarker and change the radius of your markers depended on current zoomlevel. But that's rather ugly in my opinion.

Categories

Resources