Dynamic simplification with projection in D3 - javascript

I am drawing an SVG map with D3 using the d3.geo.mercator() projection.
I also use a zoom behaviour with the map which applies a transform to the <g> object holding all paths of the map.
After looking at examples of dynamic simplification by Mike Bostock (http://bl.ocks.org/mbostock/6252418) I wonder whether I can apply such an algorithm in my case to redraw the geometry with fewer points when it's zoomed out?
In all examples I've seen, there is a simplify function which skips negligible points and plots the rest as it is, and that function is used in var path = d3.geo.path().projection(simplify). I can't use it like that since I need it to be applied on top of already existing projection = d3.geo.mercator().scale(*).translate([*,*]).
How should I use dynamic simplification with existing projection?

According to the example you quoted, Dynamic Simplification II
The simplify function would be something like
var simplify = d3.geo.transform({
point: function(x, y, z) {
if (z >= area) {
this.stream.point(x, y);
}
}
});
Where area is a treshold variable that you can set beforehand or modify dinamically according to zoom.
Then you would use it on the projection method of d3.geo.path() like
var path = d3.geo.path()
.projection(simplify);
That's more or less the situation you described in your answer. Now, according to Dynamic Simplification IV, the projection method could also be defined as
var path = d3.geo.path()
.projection({
stream: function(s) {
return simplify.stream(s);
}
});
This is exactly the same as before. It's just "expanding" the default methods. d3.geo.path always calls the projection stream method, so you can declare your own stream and forward it to simplify.stream.
Now, you say you need to reproject your path using d3.geo.mercator().
var mercatorProjection = d3.geo.mercator().scale(*).translate([*,*]);
No problem: the streams are chainable. You can do:
var path = d3.geo.path()
.projection({
stream: function(s) {
return simplify.stream(mercatorProjection.stream(s));
}
});
As well as:
var path = d3.geo.path()
.projection({
stream: function(s) {
return mercatorProjection.stream(simplify.stream(s));
}
});
The only difference being that the treshold area would have to be calculated differently if you're dealing with WGS84, pixels or another coordinate system.
Important Caveat, the z parameter in the simplify function isn't the altitude. It is the area of the triangle defined by each point, a precalculated value that's part of TopoJSON sweetness.
I'm afraid this means you can't rely on this example to simplify regular geoJSON. You'll have to add your own logic to calculate each point's related area (if you want to apply Visvalingam's algorithm) or the distance to the closest point (if you want to apply Douglas-Peucker algorithm) or implement your own algorithm.
Good luck.

Related

How to get the position X and Y from a polygon element SVG With javascript? [duplicate]

I'm using the SVG located at http://upload.wikimedia.org/wikipedia/commons/3/32/Blank_US_Map.svg in a project and interacting with it with d3.js. I'd like to create a click to zoom effect like http://bl.ocks.org/2206590, however that example relies on path data stored in a JSON object to calculate the centroid. Is there any way to load path data in d3 from an existing SVG to get the centroid?
My (hackish) attempt so far:
function get_centroid(sel){
var coords = d3.select(sel).attr('d');
coords = coords.replace(/ *[LC] */g,'],[').replace(/ *M */g,'[[[').replace(/ *z */g,']]]').replace(/ /g,'],[');
return d3.geo.path().centroid({
"type":"Feature",
"geometry":{"type":"Polygon","coordinates":JSON.parse(coords)}
});
}
This seems to work on some states, such as Missouri, but others like Washington fail because my SVG data parsing is so rudimentary. Does d3 support something like this natively?
The D3 functions all seem to assume you're starting with GeoJSON. However, I don't actually think you need the centroid for this - what you really need is the bounding box, and fortunately this is available directly from the SVG DOM interface:
function getBoundingBoxCenter (selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
var element = selection.node();
// use the native SVG interface to get the bounding box
var bbox = element.getBBox();
// return the center of the bounding box
return [bbox.x + bbox.width/2, bbox.y + bbox.height/2];
}
This is actually slightly better than the true centroid for the purpose of zooming, as it avoids some projection issues you might otherwise run into.
The accepted answer was working great for me until I tested in Edge. I can't comment since I don't have enough karma or whatever but was using this solution and found an issue with Microsoft Edge, which does not use x or y, just top/left/bottom/right, etc.
So the above code should be:
function getBoundingBoxCenter (selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
var element = selection.node();
// use the native SVG interface to get the bounding box
var bbox = element.getBBox();
// return the center of the bounding box
return [bbox.left + bbox.width/2, bbox.top + bbox.height/2];
}
From here
The solution is to use the .datum() method on the selection.
var element = d3.select("#element");
var centroid = path.centroid(element.datum());

Need to scale already projected data using d3 geoPath.projection(null)

Based on d3 (ver 1.4) documentation https://github.com/d3/d3-geo/blob/master/README.md#geoProjection , the d3 projection should be set to null to use the data's raw coordinates. How do I scale if the projection looks correct using null? Here is the code:
var path = d3.geoPath()
.projection(null)
.context(context);
bands.features.forEach(function(d, i) {
context.beginPath();
context.fillStyle = colors[i];
context.globalAlpha = .5;
path(d);
context.fill();
});
I have tried defining my own projection but the projection looks incorrect. Here is the code
var project = d3.geoProjection(function(x,y){
return [x,y]
});
var path = d3.geoPath()
.projection(project)
.context(context);
I would take a look at d3.geoTransform which should be better suited for showing already projected Cartesian data than d3.projection. From Mike Bostock:
But what if your geometry is already planar? That is, what if you just want to take projected geometry, but still translate or scale it to fit the viewport?
You can implement a custom geometry transform to gain complete control over the projection process.
To see a better example than what I can do here (and to read the rest of the quote), see this Bl.ock
For example, for your case you might use something like:
function scale (scaleFactor) {
return d3.geoTransform({
point: function(x, y) {
this.stream.point(x * scaleFactor, y * scaleFactor);
}
});
}
path = d3.geoPath().projection(scale(2));
As for why the custom projection shown
var project = d3.geoProjection(function(x,y){
return [x,y] });
changes/distorts the projection, I do not know (I had similar results testing this answer), but if this projection's output is useable, it can be scaled fairly easily:
var project = d3.geoProjection(function(x,y){
return [x,y] }).scale(1000);
Thanks for your suggestion. My problem turned out to not be a scale problem but a matrix transformation issue. I am using the d3marchingsquares.isobands software and I needed to transpose my data prior to sending it into marching squares.
My project is create a map similar to https://earth.nullschool.net/ but using Google Maps.
I am adapting Roger Veciana's "Isobands from a geotiff with d3" demo http://bl.ocks.org/rveciana/de0bd586eafd7fcdfe29227ccbdcd511. This is almost complete but having issues resizing the canvas layer. Next I am going to overlay Danny Cochran's windable canvas layer over the temperature contours. I updated Danny Cochran's code to work with the latest Google Map's version.

Animate a circle being drawn using Paper.js

I'm trying to animate a circle being drawn using Paper.js.
As the circle tool is just a quick access for instantiating a path constructed via moveTo/arcTo etc, there are no arguments to support start and end angles (for open pie chart-like circles).
What I am looking for is a way to animate the circle being drawn from it's first point to an angle of my choice at a certain radius.
The actual canvas specification allows for explicit startAngle and endAngle to be specified. If this was the case within Paper.js I could easily achieve what I am looking for. However, within Paper.js I have yet to come across a method of replicating such control. I created something in Fabric.js that worked as Fabric's implementation of the circle shape used the same attributes as the arc command in the specification.
Does anyone know of a way this can be achieved so I can animate the endAngle?
Here's a conversion function that accepts html5 canvas arc arguments and returns the from, through, to arguments needed for a Paper.js arc.
function canvasArcToPaperArc(cx,cy,radius,startAngle,endAngle,strokecolor){
var startX=cx+radius*Math.cos(startAngle);
var startY=cy+radius*Math.sin(startAngle);
var endX=cx+radius*Math.cos(endAngle);
var endY=cy+radius*Math.sin(endAngle);
var thruX=cx+radius*Math.cos((endAngle-startAngle)/2);
var thruY=cy+radius*Math.sin((endAngle-startAngle)/2);
var from = new Point(startX,startY);
var through = new Point(thruX,thruY);
var to = new Point(endX,endY);
return({from:from, through:through, to:to});
}

How can I apply an SVGMatrix to an array of points?

Are there built-in libraries to multiply a vector of points by an SVGMatrix?
I have an SVG drawing that has been scaled, and I want to annotate that drawing in its original coordinate system with a line that has a fixed width in screen space. (I.e. the line should not change width when zooming in or out, but lines in the image do, of course.) So, my approach is to transform the image inside a , and then take my array of points and apply the same transformation, then create a new path object at the root level using these transformed points.
I'm looking for the cleanest way to do this.
The svg element has methods from creating matrix objects and point objects. The matrix object has methods for matrix operations (e.g. multiply, translate, scale, etc). The point object has method to apply matrix transform.
For example...
var svg = document.getElementById("mySvg");
var matrix1 = svg.createSVGMatrix();
var matrix2 = matrix1.translate(2, 3);
var point1 = svg.createSVGPoint();
point1.x = 1;
point1.y = 1;
var point2 = point1.matrixTransform(matrix2);
Documentation for the matrix and point objects can be found at...
http://www.w3.org/TR/SVG/single-page.html#coords-InterfaceSVGPoint
http://www.w3.org/TR/SVG/single-page.html#coords-InterfaceSVGMatrix

Equivalent of canvas quadraticCurveTo in SVG

I am working on a plugin to allow "natural looking" signatures to be drawn using mouse or touch. When confirmed by the user, the result will be a stored SVG that can then be displayed in place of the "Click to sign" button.
The attached JSFiddle http://jsfiddle.net/TrueBlueAussie/67haj4nt/3/ shows a testbed for what I am trying to do. The SVG generated image should look close to the original canvas paths.
The first div contains a canvas, in which I draw some multiple-segment lines (e.g. paths). Using quadraticCurveTo, and a midpoint for the control point, I draw the lines with smooth curves. This works just fine.
The key part of the curved line drawing is:
$.each(lines, function () {
if (this.length > 0) {
var lastPoint = this[0];
ctx.moveTo(lastPoint[0], lastPoint[1]);
for (var i = 1; i < this.length; i++) {
var point = this[i];
var midPoint = [(lastPoint[0] + point[0]) / 2, (lastPoint[1] + point[1]) / 2];
ctx.quadraticCurveTo(lastPoint[0], lastPoint[1], midPoint[0], midPoint[1]);
lastPoint = point;
}
// Draw the last line straight
ctx.lineTo(lastPoint[0], lastPoint[1]);
}
});
I have tried multiple options for SVG generation of the same output, but I am stumped on how to convert the same sets of points to equivalent curved lines. Quadratic Beziers require "proper" control points, but I would prefer to use the far simpler mid-points if possible.
Any ideas? Is this possible or will I have to convert both to use Beziers with calculated control point(s). Is there a simple way to calculate control points that will do the same job?
jQuery or raw JavaScript solutions are fine, but you need to demonstrate in the JSFiddle provided :)
It's just a bug in your code. You are not updating lastPoint in your SVG version.
http://jsfiddle.net/67haj4nt/4/
And if you update the SVG version to match the canvas version, you get identical curves.
http://jsfiddle.net/67haj4nt/5/

Categories

Resources