diff --git a/README.md b/README.md index 2e022237..526de2b3 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ bertin.draw({ #### Parameters -- **projection**: a d3 function or string defining the map projection. Refer [d3-geo-projection](https://github.com/d3/d3-geo-projection) and [spatialreference.org](https://spatialreference.org/) for more detailed explanation. (default: d3.geoEquirectangular() except if you use tiles. in this case, the projection is automatically set to d3.geoMercator()). Moreover, if you define projection as "none", you can display a basemap already projected. [Example](https://observablehq.com/@neocartocnrs/bertin-js-projections). +- **projection**: a d3 function or string defining the map projection. Refer [d3-geo-projection](https://github.com/d3/d3-geo-projection) and [spatialreference.org](https://spatialreference.org/) for more detailed explanation. (default: d3.geoEquirectangular() except if you use tiles. in this case, the projection is automatically set to d3.geoMercator()). Moreover, if you define projection as "user", you can display a basemap already projected. [Example](https://observablehq.com/@neocartocnrs/bertin-js-projections). - **width**: width of the map (default:1000); - **extent**: a feature or a bbox array defining the extent e.g. a country or [[112, -43],[153, -9]] (default: null) - **margin**: margin around features to be displayed. This option can be useful if the stroke is very heavy (default: 1) diff --git a/src/draw.js b/src/draw.js index df63fda3..ae0cb14b 100644 --- a/src/draw.js +++ b/src/draw.js @@ -8,8 +8,8 @@ const d3 = Object.assign({}, d3selection, d3geo, d3geoprojection); import { getheight } from "./helpers/height.js"; import { figuration } from "./helpers/figuration.js"; import { getcenters } from "./helpers/centroids.js"; -import { proj4d3 } from "./helpers/proj4d3.js"; import { bbox } from "./bbox.js"; +import { getproj } from "./projections/projections.js"; // Layers import { graticule } from "./layers/graticule.js"; @@ -35,28 +35,14 @@ import { logo } from "./layers/logo.js"; // Main export function draw({ params = {}, layers = {} } = {}) { - // default global paramaters - - // projection - let projection = params.projection - ? params.projection - : d3.geoEquirectangular(); // default - - if (typeof projection === "string" && projection !== "none") { - projection = proj4d3(projection); - } - - // Use projected geometries - - if (typeof projection === "string" && projection === "none") { - projection = d3.geoIdentity().reflectY(true); - } - - // if tiles used, the projection is setted to d3.geoMercator() - + // projections + let projection = params.projection; const types = layers.map((d) => d.type); if (types.includes("tiles")) { projection = d3.geoMercator(); + } else { + projection = getproj(projection); + console.log(projection); } // extent @@ -160,7 +146,7 @@ export function draw({ params = {}, layers = {} } = {}) { } // ---------------------------------------- - layers.reverse().forEach((layer) => { + [...layers].reverse().forEach((layer) => { // Graticule if (layer.type == "graticule") { graticule( diff --git a/src/helpers/proj4d3.js b/src/helpers/proj4d3.js deleted file mode 100644 index 0c9cb5f1..00000000 --- a/src/helpers/proj4d3.js +++ /dev/null @@ -1,45 +0,0 @@ -// proj4d3() is a function developped by @fil. See https://observablehq.com/@fil/proj4js-d3 -import proj4 from "proj4"; -//const proj4 = Object.assign({}, proj4); - -import * as d3geo from "d3-geo"; -const d3 = Object.assign({}, d3geo); - - -let epsg2154 = "+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs" -let epsg3035 = "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +units=m +no_defs" -let epsg3857 = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs" -let epsg27700 = "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs" -let epsg42304 = "+proj=lcc +lat_1=49 +lat_2=77 +lat_0=49 +lon_0=-95 +x_0=0 +y_0=0 +datum=NAD83 +units=m +no_defs " - -proj4.defs([ ['EPSG:2154', `+title=France Lambert93 ${epsg2154}`] ]) -proj4.defs([ ['EPSG:3035', `+title=Europa (ETRS89/LAEA) ${epsg3035}`] ]) -proj4.defs([ ['EPSG:3857', `+title=WGS 84 / Pseudo-Mercator ${epsg3857}`] ]) -proj4.defs([ ['EPSG:27700', `+title=British National Grid -- United Kingdom ${epsg27700}`] ]) -proj4.defs([ ['EPSG:42304', `+title=NAD83 / NRCan LCC Canada ${epsg42304}`] ]) - - -export function proj4d3(proj4string) { - // from Philippe Rivière : https://observablehq.com/@fil/proj4js-d3 - let raw - - if (+proj4string == proj4string) - raw = proj4('EPSG:' + proj4string) - - if (!raw) - raw = proj4(proj4string) - - const degrees = 180 / Math.PI, - radians = 1 / degrees, - p = function(lambda, phi) { - return raw.forward([lambda * degrees, phi * degrees]) - } - p.invert = function(x, y) { - return raw.inverse([x, y]).map(function(d) { - return d * radians - }) - } - const projection = d3.geoProjection(p).scale(1) - projection.raw = raw - return projection -} diff --git a/src/projections/hoaxiaoguang.js b/src/projections/hoaxiaoguang.js new file mode 100644 index 00000000..3b58430d --- /dev/null +++ b/src/projections/hoaxiaoguang.js @@ -0,0 +1,16 @@ +import * as d3geoprojection from "d3-geo-projection"; +const d3 = Object.assign({}, d3geoprojection); + +// Approximative projection of HoaXiaoguang (thanks to Fil/@recifs) + +export function HoaXiaoguang() { + const projection = d3 + .geoHufnagel() + .a(0.8) + .b(0.35) + .psiMax(50) + .ratio(1.6) + .angle(90) + .rotate([110, -200, 90]); + return projection; +} diff --git a/src/projections/polar.js b/src/projections/polar.js new file mode 100644 index 00000000..185428a9 --- /dev/null +++ b/src/projections/polar.js @@ -0,0 +1,12 @@ +import * as d3geo from "d3-geo"; +const d3 = Object.assign({}, d3geo); + +export function Polar() { + const projection = d3 + .geoAzimuthalEquidistant() + .scale(190) + .rotate([0, -90]) + .clipAngle(150); + + return projection; +} diff --git a/src/projections/proj4d3.js b/src/projections/proj4d3.js new file mode 100644 index 00000000..8cd7d3aa --- /dev/null +++ b/src/projections/proj4d3.js @@ -0,0 +1,48 @@ +// proj4d3() is a function developped by @fil. See https://observablehq.com/@fil/proj4js-d3 +import proj4 from "proj4"; +//const proj4 = Object.assign({}, proj4); + +import * as d3geo from "d3-geo"; +const d3 = Object.assign({}, d3geo); + +let epsg2154 = + "+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"; +let epsg3035 = + "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +units=m +no_defs"; +let epsg3857 = + "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"; +let epsg27700 = + "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs"; +let epsg42304 = + "+proj=lcc +lat_1=49 +lat_2=77 +lat_0=49 +lon_0=-95 +x_0=0 +y_0=0 +datum=NAD83 +units=m +no_defs "; + +proj4.defs([["epsg:2154", `+title=France Lambert93 ${epsg2154}`]]); +proj4.defs([["epsg:3035", `+title=Europa (ETRS89/LAEA) ${epsg3035}`]]); +proj4.defs([["epsg:3857", `+title=WGS 84 / Pseudo-Mercator ${epsg3857}`]]); +proj4.defs([ + ["epsg:27700", `+title=British National Grid -- United Kingdom ${epsg27700}`], +]); +proj4.defs([["epsg:42304", `+title=NAD83 / NRCan LCC Canada ${epsg42304}`]]); + +export function proj4d3(proj4string) { + // from Philippe Rivière : https://observablehq.com/@fil/proj4js-d3 + let raw; + + if (+proj4string == proj4string) raw = proj4("epsg:" + proj4string); + + if (!raw) raw = proj4(proj4string); + + const degrees = 180 / Math.PI, + radians = 1 / degrees, + p = function (lambda, phi) { + return raw.forward([lambda * degrees, phi * degrees]); + }; + p.invert = function (x, y) { + return raw.inverse([x, y]).map(function (d) { + return d * radians; + }); + }; + const projection = d3.geoProjection(p).scale(1); + projection.raw = raw; + return projection; +} diff --git a/src/projections/projections.js b/src/projections/projections.js new file mode 100644 index 00000000..c289ee59 --- /dev/null +++ b/src/projections/projections.js @@ -0,0 +1,80 @@ +import { stringtod3proj } from "./stringtod3proj.js"; +import { Polar } from "./polar.js"; +import { HoaXiaoguang } from "./hoaxiaoguang.js"; +//import { Spilhaus } from "./spilhaus.js"; + +import { proj4d3 } from "./proj4d3.js"; +import * as d3geo from "d3-geo"; +const d3 = Object.assign({}, d3geo); + +// This function define the rules concerning projections + +export function getproj(projection) { + /* DEFAULT - the projection is not defined. + The default projection is d3.geoEquirectangular(). + */ + + if (projection === null || projection === undefined || projection === "") { + return d3.geoEquirectangular(); + } + + /* FUNCTION - if the projection is a d3.js function (outside bertin.js). + then, the function is used directly.*/ + + if (typeof projection === "function") { + return projection; + } + + // STRINGS + if (typeof projection === "string") { + projection = projection.replace(/\s/g, ""); + + /* CUSTOM projections*/ + if (projection == "Polar") { + return Polar(); + } + if (projection == "HoaXiaoguang") { + return HoaXiaoguang(); + } + // if (projection == "Spilhaus") { + // return Spilhaus()(); + // } + + /* USER projection - if he geometries use a projection system, + they are displayed in this projection. Then it is impossible + to reproject them on the fly. And you ca't use outline layer.*/ + + if (projection === "user") { + return d3.geoIdentity().reflectY(true); + } + + /* +PROJ & EPSG - in this case, proj4 and proj4d3 is used. */ + + if ( + projection.substring(0, 5) === "+proj" || + projection.substring(0, 5) === "epsg:" + ) { + return proj4d3(projection); + } + + /* D3.GEO - a d3.js function within a string, + then, it is interpreted roughly as a d3.js function. See projections/d3proj.js. + In this case, projections are managed inside bertin.js. See projection/d3proj.js*/ + + if ( + projection !== "user" && + projection.substring(0, 5) !== "+proj" && + projection.substring(0, 5) !== "epsg:" + ) { + return stringtod3proj(projection); + } + } + + /* TILES - if a tiles layer is used, + the projection is automatically set to d3.geoMercator(). + The geometries must be in geographic coordinates for this. + If the geometries are already projected, tiles are not used. + This part is managed in draw.js. */ + + // Custom cases +} diff --git a/src/projections/spilhaus.js b/src/projections/spilhaus.js new file mode 100644 index 00000000..d130ba02 --- /dev/null +++ b/src/projections/spilhaus.js @@ -0,0 +1,72 @@ +// import * as d3geoprojection from "d3-geo-projection"; +// const d3 = Object.assign({}, d3geoprojection); + +// function ellipticF(phi, m) { +// const { abs, atan, ln, PI: pi, sin, sqrt } = Math; +// const C1 = 10e-4, +// C2 = 10e-10, +// TOL = 10e-6; +// const sp = sin(phi); + +// let k = sqrt(1 - m), +// h = sp * sp; + +// // "complete" elliptic integral +// if (h >= 1 || abs(phi) === pi / 2) { +// if (k <= TOL) return sp < 0 ? -Infinity : Infinity; +// (m = 1), (h = m), (m += k); +// while (abs(h - k) > C1 * m) { +// k = sqrt(h * k); +// (m /= 2), (h = m), (m += k); +// } +// return sp < 0 ? -pi / m : pi / m; +// } +// // "incomplete" elliptic integral +// else { +// if (k <= TOL) return ln((1 + sp) / (1 - sp)) / 2; +// let g, n, p, r, y; +// (m = 1), (n = 0), (g = m), (p = m * k), (m += k); +// y = sqrt((1 - h) / h); +// if (abs((y -= p / y)) <= 0) y = C2 * sqrt(p); +// while (abs(g - k) > C1 * g) { +// (k = 2 * sqrt(p)), (n += n); +// if (y < 0) n += 1; +// (p = m * k), (g = m), (m += k); +// if (abs((y -= p / y)) <= 0) y = C2 * sqrt(p); +// } +// if (y < 0) n += 1; +// r = (atan(m / y) + pi * n) / m; +// return sp < 0 ? -r : r; +// } +// } + +// function ellipticFactory(a, b, sm, sn) { +// let m = Math.asin(Math.sqrt(1 + Math.min(0, Math.cos(a + b)))); +// if (sm) m = -m; + +// let n = Math.asin(Math.sqrt(Math.abs(1 - Math.max(0, Math.cos(a - b))))); +// if (sn) n = -n; + +// return [ellipticF(m, 0.5), ellipticF(n, 0.5)]; +// } + +// export function Spilhaus() { +// const { abs, max, min, sin, cos, asin, acos, tan } = Math; + +// const spilhausSquareRaw = function (lambda, phi) { +// let a, b, sm, sn, xy; +// const sp = tan(0.5 * phi); +// a = cos(asin(sp)) * sin(0.5 * lambda); +// sm = sp + a < 0; +// sn = sp - a < 0; +// b = acos(sp); +// a = acos(a); + +// return ellipticFactory(a, b, sm, sn); +// }; + +// return () => +// d3 +// .geoProjection(spilhausSquareRaw) +// .rotate([-66.94970198, 49.56371678, 40.17823482]); +// } diff --git a/src/projections/stringtod3proj.js b/src/projections/stringtod3proj.js new file mode 100644 index 00000000..d6293e51 --- /dev/null +++ b/src/projections/stringtod3proj.js @@ -0,0 +1,20 @@ +import * as d3geo from "d3-geo"; +import * as d3geoprojection from "d3-geo-projection"; +const d3 = Object.assign({}, d3geo, d3geoprojection); + +export function stringtod3proj(string) { + let str = string; + str = str.replace(/\s/g, ""); + + if (str.substring(0, 6) !== "d3.geo") { + if (str.indexOf(".") === -1) { + str += "()"; + } else { + str = str.replace(".", "()."); + } + str = "d3.geo" + str; + } + + const createProjection = new Function("d3", `return (${str})`); + return createProjection(d3); +}