I am using d3 to plot a route on a map:
const width = 400;
const height = width / 2;
const projection = d3.geoEquirectangular()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.pi);
const route_projection = d3.geoEquirectangular()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.pi);
//.preclip(SOME CLIPPING FUNCTION)
const route_path = d3.geoPath()
.projection(route_projection);
const path = d3.geoPath()
.projection(projection);
const zoom = d3.zoom()
.extent([
[0, 0],
[width, height]
])
.translateExtent([
[0, 0],
[width, height]
])
.scaleExtent([1, Infinity])
.on("zoom", zoomed);
let svg = d3.select('#map')
.append("svg")
.attr("width", width)
.attr("height", height);
svg.call(zoom);
svg.append("rect")
.attr("class", "background")
.attr("fill", "#b4dcfc")
.attr("width", width)
.attr("height", height);
let g = svg.append('g');
let geojson = {}
let curve = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"population": 200
},
"geometry": {
"type": "LineString",
"coordinates": [
[-56.12, -35.50],
[47.81, -25.37],
[78.71, 7.73]
]
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-56.12, -35.50]
},
"properties": {
"name": "Dinagat Islands"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [47.81, -25.37]
},
"properties": {
"name": "Dinagat Islands"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [78.71, 7.73]
},
"properties": {
"name": "Dinagat Islands"
}
}
]
}
function createMap() {
projection.fitSize([width, height], geojson);
route_projection.fitSize([width, height], geojson);
g
.attr("id", "dmas")
.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("fill", "white")
.attr("stroke", "#222")
.attr("stroke-width", 1)
.attr("vector-effect", "non-scaling-stroke")
.attr("d", path);
g.append("path")
.datum(curve)
.attr("id", "route")
.attr("d", route_path)
.attr("fill", "none")
.attr("stroke", 'red')
.attr("stroke-width", 2)
.attr("vector-effect", "non-scaling-stroke")
.attr("stroke-opacity", 1);
}
function zoomed(transform) {
g
.selectAll('path')
.attr('transform', transform.transform)
// .attr('stroke-width', 2/transform.transform.k)
}
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json")
.then(function(data) {
geojson = topojson.feature(data, data.objects.countries);
createMap();
})
<script src="https://unpkg.com/[email protected]/dist/d3-geo-polygon.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/topojson.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div id="map"></div>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
On route_projection
, I want to use a pre-clip to clamp the maximum latitude of the red route to above -40deg latitude, so it would end up as a straight line from South America to South Africa.
How can I do this?
CodePudding user response:
A clipping function won't achieve the correct effect as it will cut your features not coerce them to follow that line.
Instead, we can combine your projection with a geotransform to force the line to adhere to rules within the projected space:
const limit = d3.geoTransform({
point: function(x,y) {
this.stream.point(x, Math.min(y, projection([0,-36])[1]));
}
});
const route_projection = {
stream: function(s) {
return projection.stream(limit.stream(s));
}
};
const route_path = d3.geoPath()
.projection(route_projection);
This means we only need one equirectangular projection rather than two, we just combine it with the geoTransform stream and pass that combined stream to the 2nd path generator:
const width = 400;
const height = width / 2;
const projection = d3.geoEquirectangular()
.translate([width / 2, height / 2])
.scale((width - 1) / 2 / Math.pi);
const limit = d3.geoTransform({
point: function(x,y) {
this.stream.point(x, Math.min(y, projection([0,-36])[1]));
}
});
const route_projection = {
stream: function(s) {
return projection.stream(limit.stream(s));
}
};
const route_path = d3.geoPath()
.projection(route_projection);
const path = d3.geoPath()
.projection(projection);
const zoom = d3.zoom()
.extent([
[0, 0],
[width, height]
])
.translateExtent([
[0, 0],
[width, height]
])
.scaleExtent([1, Infinity])
.on("zoom", zoomed);
let svg = d3.select('#map')
.append("svg")
.attr("width", width)
.attr("height", height);
svg.call(zoom);
svg.append("rect")
.attr("class", "background")
.attr("fill", "#b4dcfc")
.attr("width", width)
.attr("height", height);
let g = svg.append('g');
let geojson = {}
let curve = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"population": 200
},
"geometry": {
"type": "LineString",
"coordinates": [
[-56.12, -35.50],
[47.81, -25.37],
[78.71, 7.73]
]
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-56.12, -35.50]
},
"properties": {
"name": "Dinagat Islands"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [47.81, -25.37]
},
"properties": {
"name": "Dinagat Islands"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [78.71, 7.73]
},
"properties": {
"name": "Dinagat Islands"
}
}
]
}
function createMap() {
projection.fitSize([width, height], geojson);
g
.attr("id", "dmas")
.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("fill", "white")
.attr("stroke", "#222")
.attr("stroke-width", 1)
.attr("vector-effect", "non-scaling-stroke")
.attr("d", path);
g.append("path")
.datum(curve)
.attr("id", "route")
.attr("d", route_path)
.attr("fill", "none")
.attr("stroke", 'red')
.attr("stroke-width", 2)
.attr("vector-effect", "non-scaling-stroke")
.attr("stroke-opacity", 1);
}
function zoomed(transform) {
g
.selectAll('path')
.attr('transform', transform.transform)
}
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json")
.then(function(data) {
geojson = topojson.feature(data, data.objects.countries);
createMap();
})
<script src="https://unpkg.com/[email protected]/dist/d3-geo-polygon.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/topojson.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div id="map"></div>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
Which should give: