Setting a custom projection using d3-geo-projection command line tool - d3.js

I'm trying to use geoproject to set the projection on a geojson file. Specifically I'm trying to set the projection to BCalbers (http://spatialreference.org/ref/epsg/3005/)
I see geoproject has a number of projections options i.e.
geoproject 'd3.geoAlbersUsa()' us.json \
> us-albers.json
but it is possible to set a custom projection using the command line tool? I was hoping something like this would be possible:
geoproject '+proj=aea +lat_1=50 +lat_2=58.5 +lat_0=45 +lon_0=-126 +x_0=1000000 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs' build/airzones.geojson \
> bc-albers.json
but no dice. This works with ogr2ogr
ogr2ogr -f GeoJSON -t_srs "+proj=aea +lat_1=50 +lat_2=58.5 +lat_0=45 +lon_0=-126 +x_0=1000000 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs " \

D3 geo projections support a range of generic projections (and custom projections), recreating any given specific projection is generally possible. However, when re-creating a projection, d3 projections don't replicate map units*. D3 projections create units in svg coordinate space (the projected coordinates will start from [0,0] which is the top left corner). This allows skipping the step of projecting data (often on the fly with d3), and then rescaling and translating it to show a map.
Compare: Using the referenced d3.geoAlbersUsa(), d3 will project data across roughly 960 pixels along the x axis if using the default scale. Perfect for unmodified use in an SVG - coordinates could be used as straight SVG coordinates. Using a BC Albers in ArcGIS or QGIS will project data across millions of meters.
Units aside, however, you can recreate a BC Albers proportionally scaled for web preserving the shape, distance, direction, and area of a regular BC Albers. But as d3 doesn't take projection definitions such as .prj files or other definitions, you need to use a d3's projection methods and the appropriate parameters.
For a BC Albers your parallels are: 50 and 58.5, your central longitude is -126, and your projection type is an Albers. This is all you need - the (false) easting/northing reference is to recreate map units - which should not generally be needed in a web scenario (if you do, using a more complete GIS platform would be more appropriate).
So, to set the projection, you would use:
d3.geoAlbers()
.center([0,y])
.rotate([-x,0])
.parallels([a,b])
.scale(k)
Where
x = center longitude (negative because we rotate the globe under the map)
a, b = standard parallels
k = scale factor(for a d3.geoAlbers(): whole world is 960 px across with a scale of 1070, the default scale, larger numbers expand this/zoom in)
y = centering latitude.
Note: y does not alter the projection, it merely translates it - the y reference for a BC Albers is south of BC, it is just a reference for northings, as it has no impact on map shape, area, distance, or direction. For a BC Albers, I would probably choose 50.5 as it is half way between the Yukon border and the Washington border which are the northern and southern limits of BC (well, excepting Vancouver Island and some of the Gulf Islands, so let's say 50 degrees north, sorry I forgot about you Victoria).
Also remember the the projection functionality of d3 assumes data that is unprojected (or "projected" in WGS84), consisting of long lat pairs.
You can see how Mike Bostock uses these methods in his command line cartography article here:
geoproject 'd3.geoConicEqualArea().parallels([34, 40.5]).rotate([120, 0]).fitSize([960, 960], d)' < ca.json > ca-albers.json
fitSize in this exmaple scales and translates the features to the specified bounding box - this translate and scale does not alter the projection parameters, and like the y coordinate in the center method, does not alter distance, area, shape, or angle (well, distance and area remain proportional to a proper BC Albers).
*You could recreate map units (false eastings/northings might require some custom projection work), but this is not the platform for it really, it would be easier to use many other platforms.
See also this question and answer:Converting EPSG projection bounds to a D3.js.

Related

How to translate MKT expression to D3 options on Albers projection?

This is the standard MKT expression (here also translated to Proj.4 string) of Albers conicEqualArea for official Statistical Grid of Brazil:
PROJCS["Conica_Equivalente_de_Albers_Brasil",
GEOGCS["GCS_SIRGAS2000",
DATUM["D_SIRGAS2000",
SPHEROID["Geodetic_Reference_System_of_1980",6378137,298.2572221009113]],
PRIMEM["Greenwich",0],
UNIT["Degree",0.017453292519943295]],
PROJECTION["Albers"],
PARAMETER["standard_parallel_1",-2],
PARAMETER["standard_parallel_2",-22],
PARAMETER["latitude_of_origin",-12],
PARAMETER["central_meridian",-54],
PARAMETER["false_easting",5000000],
PARAMETER["false_northing",10000000],
UNIT["Meter",1]]
The DATUM is the WGS 84 ("SIRGAS2000" is a alias for it).
How to translate all details to the D3.js v5 parametrization?
I try the obvious, as center and parallels, but it was not sufficient
var projection = d3.geoConicEqualArea()
.parallels([-2,-22]) // IS IT?
.scale(815)
//.rotate([??,??]) // HERE THE PROBLEM...
.center([-54, -12]) // IS IT?
PS: where the D3 documentation for it? The D3 source-code of geoConicEqualArea() have no clues.
The parts that translate to a d3 Albers projection are as follows:
PROJECTION["Albers"],
PARAMETER["standard_parallel_1",-2],
PARAMETER["standard_parallel_2",-22],
PARAMETER["latitude_of_origin",-12],
PARAMETER["central_meridian",-54],
You have the parallels, now you need to rotate. Also note, for any D3 projection, the rotation is applied to the centering coordinates. Generally, you'll want to rotate on the x and center on the y:
d3.geoAlbers()
.parallels([-2,-22])
.center([0,-12])
.rotate([54,0])
.translate([width/2,height/2])
.scale(k)
I've rotated in the opposite direction along the x axis (rotated the earth under me so that I'm overtop of the central meridian, hence my rotation by -x). I've then centered on the y. Lastly I translate so that the intersection of the central longitude and meridian is centered in the map and apply a scale value that is appropriate.
If I want to center on a different area but keep the projection the same, I can modify projection.center(), but keep in mind that the coordinates provided here are relative to the rotation. I can also use projection.fitSize() or projection.fitExtent(), both of which set 'translate' and 'scale' values for the projection. None of center/scale/translate change the distortion in the D3 projection.
Of course this isn't a true replication of your projection as the coordinate space units are pixels, you will remain unable to measure distances in meters directly without some extra work.
See also

Points on d3 map not on the same scale or part of the svg

I have been trying to make a d3 map by cobbling together code from others. See my attempt here on Github: https://gist.github.com/gailzdesign/018f3eabcabad64f3836727748a9a552
I've read through several entries on Stack Overflow, including this one: Plotting points on a map with D3
And another one that had points plotted in San Fran, California, USA.
But I still can't figure out what I'm doing wrong. I can see the points in the DOM but the scale is way off. The circles are not even a part of the svg.
Any help would be appreciated.
The points are where they should be, the eastern edge of your geojson is about 67.59 degrees West, but your points (rocks) are further east at about 67.33 degrees West, so they aren't appended visibly within the SVG - as you note. For example, if you set your projection scale to 50000 rather than your scale variable, you'll see the points and the lake:
The points aren't visible because your auto scaling and bounding function doesn't include the points. But I'm assuming the rocks are supposed to be in the lake, so you'll need to update your locations.
If you are unable to get alignment between the lake and the points, you may have conflicting datums.
If the points are intended to be away from the lake, you might find it easier to set the scale manually, as the autoscaling only takes into account the geojson features. Alternatively you could add the points to the geojson.

Converting EPSG projection bounds to a D3.js map

Given an EPSG projection (say, this Alabama one: [http://spatialreference.org/ref/epsg/26729/][1])
How can you take the given WGS84 projection bounds in such a way that you can use them in a D3.js projection.
For example, how would you know what projection, degree of rotation or bounding box to use to show the map?
This is a fairly complex question. The answer will differ based on the spatial reference (SRS, or coordinate reference system(CRS)) system you are looking at and what your ultimate goal is.
I am using d3.js v4 in this answer
Short Answer:
For example, how would you know what projection, degree of rotation or
bounding box to use to show the map?
There is no hard and fast set of rules that encompasses all projections. Looking at the projection parameters can usually give you enough information to create a projection quickly - assuming the projection comes out of the box in d3.
The best advice I can give on setting the parameters, as when to rotate or when to center, what parallels to use etc, is to zoom way out when refining the projection so you can see what each parameter is doing and where you are looking. Then do your scaling or extent fitting. That and use a geojson validator for your bounding box, like this one.
Lastly, you could always use projected data and drop d3.geoProjection altogether (this question), if all your data is already projected in the same projection, trying to define the projection is a moot point.
Datums
I'll note quickly that the question could be complicated further if you look at differences between datums. For example, the SRS you have referenced used the NAD27 datum. A datum is a mathematical representation of the earth's shape, NAD27 will differ from NAD83 or WGS84, though all are measured in degrees, as the datum represents the three dimensional surface of the earth. If you are mixing data that uses conflicting datums, you could have some precision issues, for example the datum shift between NAD27 and NAD83 is not insignificant depending on your needs (wikipedia screenshot, couldn't link to image):
If shifts in locations due to use of multiple datums is a problem, you'll need more than d3 to convert them into one standard datum. D3 assumes you'll be using WGS84, the datum used by the GPS system. If these shifts are not a problem, then ignore this part of the answer.
The Example Projection
So, let's look at your projection, EPSG:26729:
PROJCS["NAD27 / Alabama East",
GEOGCS["NAD27",
DATUM["North_American_Datum_1927",
SPHEROID["Clarke 1866",6378206.4,294.9786982138982,
AUTHORITY["EPSG","7008"]],
AUTHORITY["EPSG","6267"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.01745329251994328,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4267"]],
UNIT["US survey foot",0.3048006096012192,
AUTHORITY["EPSG","9003"]],
PROJECTION["Transverse_Mercator"],
PARAMETER["latitude_of_origin",30.5],
PARAMETER["central_meridian",-85.83333333333333],
PARAMETER["scale_factor",0.99996],
PARAMETER["false_easting",500000],
PARAMETER["false_northing",0],
AUTHORITY["EPSG","26729"],
AXIS["X",EAST],
AXIS["Y",NORTH]]
This is a pretty standard description of a projection. Each type of projection will have parameters that are specific to it, so these won't always be the same.
The most important parts of this description are:
NAD27 / Alabama East Projection name, not needed but a good reference as it's a little easier to remember than an EPSG number, and references/tools may only use a common name instead of an EPSG number.
PROJECTION["Transverse_Mercator"] The type of projection we are dealing with. This defines how the 3d coordinates representing points on the surface of the earth are translated to 2d coordinates on a cartesian plane. If you see a projection here that is not listed on the d3 list of supported projections (v3 - v4), then you have a bit of work to do in defining a custom projection. But, generally, you will find a projection that matches this. The type of projection changes whether a map is rotated or centered on each axis.
PARAMETER["latitude_of_origin",30.5],
PARAMETER["central_meridian",-85.83333333333333],
These two parameters set the center of the projection. For a transverse Mercator, only the central meridian is important. See this demo of the effect of choosing a central meridian on a transverse Mercator.
The latitude of origin is chiefly used to set the a reference point for the northnigs. The central meridian does this as well for the eastings, but as noted above, sets the central meridian in which distortion is minimized from pole to pole (it is equivalent to the equator on a regular Mercator). If you really need to have proper northings and eastings so that you can compare x,y locations from a paper map and a web map sharing the same projection, d3 is probably not the best vehicle for this. If you don't care about measuring the coordinates in Cartesian coordinate space, these parameters do not matter: D3 is not replicating the coordinate system of the projection (measured in feet as false eastings/northings) but is replicating the same shape in SVG coordinate space.
So based on the relevant parameters in the projection description, a d3.geoProjection centered on the origin of this projection would look like:
d3.geoTransverseMercator()
.rotate([85.8333,0])
.center([0,30.5])
Why did I rotate roughly 86 degrees? This is how a transverse Mercator is built. In the demo of a transverse Mercator, the map is rotated along the x axis. Centering on the x axis will simply pan the map left and right and not change the nature of the projection. In the demo it is clear the projection is undergoing a change fundamentally different than panning, this is the rotation being applied. The rotation I used is negative as I turn the earth under the projection. So this projection is centered at -85.833 degrees or 85.8333 degrees West.
Since on a Transverse Mercator, distortion is consistent along a meridian, we can pan up down and not need to rotate. This is why I use center on the y axis (in this case and in others, you could also rotate on the y axis, with a negative y, as this will spin the cylindrical projection underneath the map, giving the same result as panning).
If we are zoomed out a fair bit, this is what the projection looks like:
It may look pretty distorted, but it is only intended to show the area in and near Alabama. Zooming in it starts to look a lot more normal:
The next question is naturally: What about scale? Well this will differ based on the size of your viewport and the area you want to show. And, your projection does not specify any bounds. I'll touch on bounds at the end of the answer, if you want to show the extent of a map projection. Even if the projection has bounds, they may very well not align with the area you want to show (which is usually a subset of the overall projection bounds).
What about centering elsewhere? Say you want to show only a town that doesn't happen to lie at the center of the projection? Well, we can use center. Because we rotated the earth on the x axis, any centering is relative to the central meridian. Centering to [1,30.5], will center the map 1 degree East of the central meridian (85.8333 degrees West). So the x component will be relative to the rotation, the y component will be in relation to the equator - its latitude).
If adhering to the projection is important, this odd centering behavior is needed, if not, it might be easier to simply modify the x rotation so that you have a projection that looks like:
d3.geoTransverseMercator()
.center([0,y])
.rotate([-x,0])
...
This will be customizing the transverse Mercator to be optimized for your specific area, but comes at the cost of departing from your starting projection.
Different Projections Types
Different projections may have different parameters. For example, conical projections can have one (tangent) or two (secant) lines, these represent the points where the projection intersects the earth (and thus where distortion is minimized). These projections (such as an Albers or Lambert Conformal) use a similar method for centering (rotate -x, center y) but have the additional parameter to specify the parallels that represent the tangent or secant lines:
d3.geoAlbers()
.rotate([-x,0])
.center([0,y])
.parallels([a,b])
See this answer on how to rotate/center an Albers (which is essentially the same for all conical projections that come to mind at the moment).
A planar/azimuthal projeciton (which I haven't checked) is likely to be centered only. But, each map projection may have a slightly different method in 'centering' it (usually a combination of .rotate and .center).
There are lots of examples and SO questions on how to set different projection types/families, and these should help for most specific projections.
Bounding Boxes
However, you may have a projection that specifies a bounds. Or more likely, an image with a bounds and a projection. In this event, you will need to specify those bounds. This is most easily done with a geojson feature using the .fitExtent method of a d3.geoProjection():
projection.fitExtent(extent, object):
Sets the projection’s scale and translate to fit the specified GeoJSON object in the center of the given extent. The extent is specified as an array [[x₀, y₀], [x₁, y₁]], where x₀ is the left side of the bounding box, y₀ is the top, x₁ is the right and y₁ is the bottom. Returns the projection.
(see also this question/answer)
I'll use the example in the question here to demonstrate the use of a bounding box to help define a projection. The goal will be to project the map below with the following knowledge: its projection and its bounding box (I had it handy, and couldn't find a good example with a defined bounding box quick enough):
Before we get to the bounding box coordinates however, let's take a look at the projection. In this case it is something like:
PROJCS["ETRS89 / Austria Lambert",
GEOGCS["ETRS89",
DATUM["European_Terrestrial_Reference_System_1989",
SPHEROID["GRS 1980",6378137,298.257222101,
AUTHORITY["EPSG","7019"]],
AUTHORITY["EPSG","6258"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.01745329251994328,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4258"]],
UNIT["metre",1,
AUTHORITY["EPSG","9001"]],
PROJECTION["Lambert_Conformal_Conic_2SP"],
PARAMETER["standard_parallel_1",49],
PARAMETER["standard_parallel_2",46],
PARAMETER["latitude_of_origin",47.5],
PARAMETER["central_meridian",13.33333333333333],
PARAMETER["false_easting",400000],
PARAMETER["false_northing",400000],
AUTHORITY["EPSG","3416"],
AXIS["Y",EAST],
AXIS["X",NORTH]]
As we will be letting d3 choose the scale and center point based on the bounding box, we only care about a few parameters:
PARAMETER["standard_parallel_1",49],
PARAMETER["standard_parallel_2",46],
These are the two secant lines, where the map projection intercepts the surface of the earth.
PARAMETER["central_meridian",13.33333333333333],
This is the central meridian, the number we will use for rotating the projection along the x axis (as one will do for all conical projections that come to mind).
And most importantly:
PROJECTION["Lambert_Conformal_Conic_2SP"],
This line gives us our projection family/type.
Altogether this gives us something like:
d3.geoConicConformal()
.rotate([-13.33333,0]
.parallels([46,49])
Now, the bounding box, which is defined by these limits:
East: 17.2 degrees
West: 9.3 degrees
North: 49.2 degrees
South: 46.0 degrees
The .fitExtent (and .fitSize) methods take a geojson object and translate and scale the projection appropriately. I'll use .fitSize here as it skips margins around the bounds (fitExtent allows provision of margins, that's the only difference). So we need to create a geojson object with those bounds:
var bbox = {
"type": "Polygon",
"coordinates": [
[
[9.3, 49.2], [17.2, 49.2], [17.2, 46], [9.3, 46], [9.3,49.2]
]
]
}
Remember to use the right hand rule, and to have your end point the same as your start point (endless grief otherwise).
Now all we have to do is call this method and we'll have our projection. Since I'm using an image to validate my projection parameters, I know the aspect ratio I want. If you don't know the aspect ratio, you may have some excess width or height. This gives me something like:
var projection = d3.geoConicConformal()
.parallels([46,49])
.rotate([-13.333,0])
.fitSize([width,height],bbox)
And a happy looking final product like (keeping in mind a heavily downsampled world topojson):

d3 mercator geo bounds to pixel bounds

I was wondering whether there is a D3-API call that translates a d3.geo.bounds() array to a path.bounds pixel array given a width and height of the map?
I was unable to find such a call in the API documentation. I know that there are projections that should to the trick, but I failed in getting to the actual pixel bound from a geo bound.
Also, is there an API implementation of computing the lat/long centroid of a series of features?
I appreciate any help!
Example
Assume the user wants to zoom Germany and Poland to fullscreen. My computed lat-long boundary is:
lat:47.27148371456806 long: 5.850585058505857
(1321.6041604160416,246.58117844670664)
lat:55.06843452896922 long: 24.140414041404142
(1451.6651665166517,157.87336991900156)
Checking with google-maps, the lat-long coordinates are correct. However, the computed pixel values (using the approach from https://stackoverflow.com/a/14457180/974815) seem weird to me. The screen resolution is 2560x1258.
I want to translate the lat-long boundaries to pixel boundaries in order to compute the offset for the mercator projection:
var offset = [
this.width - (bounds[0][0] + bounds[1][0])/2,
this.height - (bounds[0][1] + bounds[1][1])/2
];
return d3.geo.path().projection(
d3.geo.mercator()
.center(this.__computeCentroid(features))
.scale(scale)
.translate(offset)
);
Best,
Sebastian
The geographic coordinates of features are translated into pixel (projection) coordinates using one of the projections available. You can either use the projection to translate the geographic bounds into projected bounds, or use path.bounds() directly to get that information. That is, instead of passing the feature to d3.geo.bounds(), you pass it to the .bounds() method of the d3.geo.path() that uses your projection.
There is no function to compute the centroid of a series of features. You can either combine the features into one (preferably using a GIS program) and get the centroid of that, or get the centroids of all the features and then get the centroid of the centroids by averaging the coordinates.

Using d3, what is the easiest way to draw a 10000 km2 polygon on a map?

I would like to illustrate how big different areas are by drawing appropriately sized 'squares' over a map of the UK. See a non SVG example of the idea.
Ideally I'd like to use d3.js to draw the map and the 'squares' using SVG. Ideally I'd like the 'squares' to be polygons that have been appropriately adjusted for the distortion in the map projection.
I get how to draw the map and to project a polygon, so I guess what I'm trying to work out is a javascript function that will take an area, a latitude and a longitude and return a geoJSON polygon that I can then project:
function square(area, latitude, longitude) {
... something ...
return geojson_polygon;
}
Is this part of any pre-existing library? or can you point me towards some example code that I can crib? or just the maths that I need to implement?
I wouldn't try to draw a rectangle as a geojson object; just use your map projection to turn the latitude and longitude into x y coordinates for the svg and draw the rectangles on a separate layer.
To make the rectangle the correct size, you could just eyeball different heights and widths. Alternatively, do a little math (http://jamesmccaffrey.wordpress.com/2012/03/03/the-area-bounded-by-geographical-latitude-longitude-points/) to find the latitude and longitude of a rectangle with the correct area, convert those numbers to x y svg coordinates and use those to draw the rectangle.

Resources