Eliminate sudden additions/deletions in D3 line chart transition - transition

You can see this code in action here: http://bl.ocks.org/2626142
This code draws a line chart, then transitions between 3 sample data sets. When moving from a small data set to a larger one, the extra data points suddenly appear instead of smoothly unfolding from the existing line.
When moving from a larger data set to a smaller one, the line is suddenly truncated before transitioning to fill the whole chart.
With this code there are sudden additions and deletions to the line and gridlines. How do I eliminate those?
var data = [
[0,2,3,2,8],
[2,4,1,5,3],
];
var data2 = [
[0,1,2,3,4,5],
[9,8,7,6,5,6],
];
var data3 = [
[1,3,2],
[0,8,5],
];
var w = 300,
h = 100;
var chart = d3.select('body').append('div')
.attr('class', 'chart')
.append('svg:svg')
.attr('width', w)
.attr('height', h);
var color = d3.scale.category10();
function drawdata(data, chart) {
var num = data[0].length-1;
var x = d3.scale.linear().domain([0, num]).range([0,w]);
var y = d3.scale.linear().domain([0, 10]).range([h, 0]);
var line = d3.svg.line()
.x(function(d, i) { return x(i); })
.y(function(d) { return y(d); });
var flat = d3.svg.line()
.x(function(d, i) { return x(i); })
.y(y(-1));
var lines = chart.selectAll('.line')
.data(data);
lines.enter().append('path')
.attr('class', 'line')
.style('stroke', function(d,i) { return color(i); })
.attr('d', line);
lines.transition()
.ease('linear')
.duration(500)
.attr('d', line);
lines.exit().remove();
// legend
var ticks = chart.selectAll('line')
.data(x.ticks(num));
ticks.enter().append('line')
.attr('x1', x)
.attr('x2', x)
.attr('y1', 0)
.attr('y2', h)
.attr('class', 'rule');
ticks.transition()
.ease('linear')
.duration(500)
.attr('x1', x)
.attr('x2', x)
.attr('y1', 0)
.attr('y2', h);
ticks.exit().remove();
}
var dats = [data, data2, data3];
function next() {
var it = dats.shift();
dats.push(it);
drawdata(it, chart);
}
setInterval(next, 2000);
next();

I faced a similar problem recently, and solved it using a custom interpolator for paths:
// Add path interpolator to d3
d3.interpolators.push(function(a, b) {
var isPath, isArea, interpolator, ac, bc, an, bn;
// Create a new array of a given length and fill it with the given value
function fill(value, length) {
return d3.range(length)
.map(function() {
return value;
});
}
// Extract an array of coordinates from the path string
function extractCoordinates(path) {
return path.substr(1, path.length - (isArea ? 2 : 1)).split('L');
}
// Create a path from an array of coordinates
function makePath(coordinates) {
return 'M' + coordinates.join('L') + (isArea ? 'Z' : '');
}
// Buffer the smaller path with coordinates at the same position
function bufferPath(p1, p2) {
var d = p2.length - p1.length;
// Paths created by d3.svg.area() wrap around such that the 'end'
// of the path is in the middle of the list of coordinates
if (isArea) {
return fill(p1[0], d/2).concat(p1, fill(p1[p1.length - 1], d/2));
} else {
return fill(p1[0], d).concat(p1);
}
}
// Regex for matching the 'd' attribute of SVG paths
isPath = /M-?\d*\.?\d*,-?\d*\.?\d*(L-?\d*\.?\d*,-?\d*\.?\d*)*Z?/;
if (isPath.test(a) && isPath.test(b)) {
// A path is considered an area if it closes itself, indicated by a trailing 'Z'
isArea = a[a.length - 1] === 'Z';
ac = extractCoordinates(a);
bc = extractCoordinates(b);
an = ac.length;
bn = bc.length;
// Buffer the ending path if it is smaller than the first
if (an > bn) {
bc = bufferPath(bc, ac);
}
// Or, buffer the starting path if the reverse is true
if (bn > an) {
ac = bufferPath(ac, bc);
}
// Create an interpolater with the buffered paths (if both paths are of the same length,
// the function will end up being the default string interpolator)
interpolator = d3.interpolateString(bn > an ? makePath(ac) : a, an > bn ? makePath(bc) : b);
// If the ending value changed, make sure the final interpolated value is correct
return bn > an ? interpolator : function(t) {
return t === 1 ? b : interpolator(t);
};
}
});
Here's what the original gist looks like with the new interpolator: http://bl.ocks.org/4535474.
Its approach is to 'buffer' the smaller dataset's path by inserting zero-length line segments at the beginning. The effect is that new segments expand out of a single point at the start of the line, and unused segments similarly collapse down to a single point.
Transitioning between datasets of different sizes (apparently) isn't a common problem, and doesn't have a universal solution. Because I was visualizing time-series data and transitioning between daily/weekly/monthly intervals, I needed the segments towards the end of the path to maintain visual continuity. I can imagine a case in which you'd want to do the same for the beginning of the path, or perhaps expand/contract the path by uniformly buffering segments throughout. Either way the same approach will work.

Related

Forward pass neural network output values always converge to a mean

I thought the best way to learn neural networks would be to create one from scratch in JavaScript using the MNIST database of images, each image displaying a single handwritten number from 0 to 9. Each image is 784 pixels, each pixel value being either 1 (white) or 0 (black).
Now before I start posting any code, I wanted to first keep things abstract and talk out my problem to see if I'm just missing something very basic and embarrassing.
NN basic specs are:
784 input values 2 hidden layers
each containing 50 neurons 10
output neurons representing labels 0 to 9
beta = 0 in every layer
learning rate = 0.01 initial weights set using xavier
activation function = sigmoid throughout for now
When I run this NN over a 1000 images or so, all the outputs slowly descend to around 0.1 and then basically all hover there forever. When I watch a single image flow through the NN and then backprop, this actually makes sense. Most of the time, in fact 90% of the time, the true value for a single output will be zero. Therefore, for a single output, if I run 10 images through, 9 of those images will want the output to move towards truth=zero, while only 1 images will want it to move up to truth=one. I actually see happen in the numbers....you can see a single output value slowly descend towards truth=zero a bunch of times, then get a temporary bump up when truth=one.
I hope this makes sense....basically my NN would rather reduce my total error by moving all my outputs closer to truth=zero. It's almost as if I need to adjust my learning rate to be 10x greater when adjusting my weights towards truth=one...but then I suspect this would just adjust all my outputs to 0.5.
I've been through my code so many times, and I believe my math is good, but of course I could be missing something. That said...I thought I'd start here first and see if anyone else has seen this.
EDIT....raw code containing NN classes. Sorry, bit messy. Basically you call a new network class, then run network.addlayer to populate layers, neurons, and set weights. From there you can start training by call network.inputTrainingData, which accepts the binary array of 784 pixels, and the truth value for the image. And finally, you can network backPropogate after every image.
class network {
layers = [];
learning_rate = 0.01;
inputCnt = 784;
labels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
xValues = [];
truth = [];
constructor() {
}
addLayer(neuronCnt, isOutputLayer) {
//note...we're not adding an "input layer....why do that when you can just feed the inputs into the first hidden layer?
if (isOutputLayer) {
this.layers.push(new layer(this.labels.length, this.layers[this.layers.length - 1].neurons.length, isOutputLayer));
} else if(this.layers.length) {
this.layers.push(new layer(neuronCnt, this.layers[this.layers.length - 1].neurons.length, isOutputLayer));
} else {
this.layers.push(new layer(neuronCnt, this.inputCnt, isOutputLayer));
}
}
get numberOfLayers() {
return this.layers.length;
}
inputTrainingData(data, truth) {
this.xValues = data;
this.xValues.forEach(function (x, i) {
if (x == 0) { x = 0.0001; };
})
this.truth = truth;
var $this = this;
this.layers.forEach(function (layer, index) {
layer.input($this.xValues);
$this.xValues = layer.output();
});
}
backPropogate() {
var $this = this;
var prev_layer = 0;
var t = this.getTruth();
for (var i = this.layers.length-1; i >= 0; i--) {
this.layers[i].backPropogate(t, $this.learning_rate, prev_layer);
prev_layer = this.layers[i];
}
}
getTruth() {
var truth = [];
var $this = this;
this.labels.forEach(function (label, index) {
if ($this.truth == label) {
truth.push(1);
} else {
truth.push(0);
}
})
return truth;
}
}
class layer {
neurons = [];
inputs = [];
outputs = [];
final_deriv = [];
hidden_deriv = [];
beta = 0; //Math.random();
isOutputLayer = false;
constructor(neuronCnt, prevLayerNeuronCnt, _isOutputLayer) {
this.isOutputLayer = _isOutputLayer;
var i;
for (i = 0; i < neuronCnt; i++) {
var newNeuron = new neuron(neuronCnt, prevLayerNeuronCnt);
this.neurons.push(newNeuron);
}
}
input(xValues) {
this.inputs = xValues;
var $this = this;
this.neurons.forEach(function (neuron, index) {
neuron.input(xValues, $this.beta);
});
}
output() {
this.outputs = [];
var $this = this;
this.neurons.forEach(function (neuron, index) {
$this.outputs.push(neuron.output($this.isOutputLayer));
})
return this.outputs;
}
backPropogate(truth, learning_rate, prev_layer) {
var $this = this;
this.neurons.forEach(function (neuron, index) {
neuron.backPropogateWeights(truth[index], learning_rate, prev_layer, index);
//neuron.backPropogateBeta(truth[index], prev_layer);
});
}
}
class neuron {
xValues = 0;
b = 0;
weights = [];
h = 0;
neuron_deriv = 1;
weights_deriv = [];
constructor(neuronCnt, prevLayerNeuronCnt) {
var i;
var xavier = 1 / prevLayerNeuronCnt;
for (i = 0; i < prevLayerNeuronCnt; i++) {
var rnd = (Math.random() - 0.5) * xavier;
//rnd = ((Math.random() * 2) - 1) * xavier;
this.weights.push(rnd);
}
}
input(xValues, beta) {
this.xValues = xValues;
this.b = beta;
}
output(isOutputLayer) {
var func = 0;
var $this = this;
this.xValues.forEach(function (x, index) {
func += x * $this.weights[index];
});
func += this.b;
var sigmoid = 1 / (1 + Math.exp(-func));
$this.h = sigmoid;
$this.neuron_deriv = (sigmoid * (1 - sigmoid));
return this.h;
}
backPropogateWeights(truth, learning_rate, prev_layer, cnt) {
var $this = this;
var prevSumError = 1;
if (prev_layer == 0) {
//var SqError = Math.pow((this.h - truth), 2)/2;
var dEdH = (this.h - truth);
this.neuron_deriv = this.neuron_deriv * dEdH;
} else {
prevSumError = 0;
prev_layer.neurons.forEach(function (neuron, index) {
prevSumError += neuron.weights_deriv[cnt];
});
}
this.weights.forEach(function (weight, index) {
$this.weights_deriv.push($this.neuron_deriv * weight * prevSumError);
});
for (var i = 0; i < this.weights.length; i++) {
var slope = $this.neuron_deriv * $this.xValues[i] * prevSumError;
var weightAdjust = learning_rate * slope;
$this.weights[i] = $this.weights[i] - weightAdjust;
}
}
}
This took a very long time to debug. It was the weights. I was using an xavier function to randomize all the weights, but when I opened up the weight range between -2.5 to 2.5, everything just started worked.
Here was my debug process if it helps anyone else:
- Repeated all my code calculations by hand to ensure code was running correctly
- Reduced entire neural network to bare minimal number of layers and neurons.
- Trained on a single image, then two, then three.
- First breakthrough was when I trained on just two images and eliminated ALL hidden layer....it actually learned!
- From here I slowly added in a layer and a few more images.
- It's at this point I started to expand my weight range....and I could see it was starting to learn.
- Finally I blew out my weight range, went back to my original setup, and boom! worked!
It's still not learning as fast I it should, based on the same implementation by Andrej Kaparthy in his ConvNetJS....but at least I'm now in a position to start tweaking.

ChartJS - line graph, position tooltip

I have the following graph with 3 datasets. Everything works fine except for one small bug. I want to tooltip to be placed only on the first dataset, as it currently is placed on the average position of all datasets.
Here is a screenshot:
I know that ChartJS has the positioners function (the one below), but I can't figure out how to ignore the other 2 datasets so the tooltip is sticked only to the first line graph
Chart.Tooltip.positioners.top = function (elements, eventPosition) {
const tooltip = this;
return ;
};
You can also define a custom position function for tooltips. like this,
Chart.Tooltip.positioners.custom = function(elements, eventPosition) {
var x = eventPosition.x;
var y = eventPosition.y;
var minDistance = Number.POSITIVE_INFINITY;
var i, len, nearestElement;
var top_element = elements[0];
for (i = 0, len = elements.length; i < len; ++i) {
console.log(elements[i].tooltipPosition().y);
if (elements[i].tooltipPosition().y < top_element.tooltipPosition().y) {
top_element = elements[i];
}
}
var tp = top_element.tooltipPosition();
x = tp.x;
y = tp.y;
return {
x: x,
y: y
};
};
Once done, you can specify it in tooltips.options.
More information :
https://www.chartjs.org/docs/latest/configuration/tooltip.html#position-modes
Hope it helps!

Accessing CSV row when using a class

*Edited with advise from below. STill working on minisming the code more. *
I am attempting to shift to using classes in my code. I have this functioning in a larger piece of code and I'm attempting to recreate the stepping action now using a class.
if (i< table.getRowCount()) {
row = table.getRow(i);
k=row.getInt("y");
x=row.getInt("x");
}
//draw white ellipse visual
ellipse(x*diam, y, diam, diam);
// step next circle up
y+=Step;
// -- if off the set upper limit of the line,
// swing right
if (frameCount%(k/Step)==0) {
x+=Step;
y=starty;
// increment row counter
i=i+1;
}
I am trying to troubleshoot why I am unable to step through variable i as the rows in the csv table.
I am expecting k to print out the data in column named "y" in the csv file.
I am expecting i to print out a number incrementing by 1 each time variable y reaches the value of k.
From printing out my variables i,k,x
k is successfully getting the data from csv file column named "y"
x is successfully getting the data from csv file column named "x"
y is successfully incrementing
i is not incrementing
y is not registering it has reached k.
my thoughts right now are that actually variable k is a marker for y to stop incrementing but should perhaps not be part of the Dot parameters in the constructor.
// An Array of Dot objects
Dot[] dots;
// A Table object
Table table;
float diam=10.0;
int i;
void setup() {
size(560, 420);
loadData();
background(0);
// set the animation speed
frameRate(10);
}
void draw() {
// Display all dots
for (Dot d : dots) {
d.display();
d.move();
}
}
void loadData() {
// Load CSV file into a Table object
// "header" option indicates the file has a header row
table = loadTable("data.csv", "header");
// The size of the array of Dot objects is determined by the total number of rows in the CSV
dots = new Dot[table.getRowCount()];
// You can access iterate over all the rows in a table
int rowCount = 0;
for (TableRow row : table.rows()) {
// You can access the fields via their column name (or index)
if (i< table.getRowCount()) {
row = table.getRow(i);
float x = row.getFloat("x");
float k = row.getFloat("y");
float d = row.getFloat("diameter");
//String n = row.getString("name");
// Make a fot object out of the data read
dots[rowCount] = new Dot(i,k, x, d);
rowCount++;
}
}
// check to see if at end of data
if (i == table.getRowCount()) {
i = 0;//if so, loop back to first row
}
}
class Dot {
float x =10.0;
float y=10.0;
float diam=10.0;
float k;
int step=10;
int startx =10;
int starty=10;
float i=0;
Dot(float i_,float k_, float x_,float diam_) {
k=k_;
x=x_;
i=i_;
diam=diam_;
}
void display() {
noStroke();
fill(255);
// draw a circle
ellipse(x*diam, y, diam, diam);
}
void move() {
// move next circle to the right
y+=step;
// -- if off the right edge of the line,
// swing down and all the way to the left
if (frameCount%(k/step)==0) {
x+=step;
y=starty;
// increment row counter
i=i+1;
}
// if off the bottom of the page, stop
if (x>=width) {
fill(0);
text("done", width/2, height/2);
noLoop();
}
}
}

Using Bing Maps Quadkeys as Openlayers 3 Tile source

I have a number of tile sources which utilise Bing Maps' Quadkey system in an old Silverlight application and I would like to use them in a new Openlayers 3 map.
I have found several examples of functions which will convert these sources for Leaflet.js but the syntax is somewhat different for OL3 and reading through the API docs indicates that there is an ol.Tile.coord class but if I understand correctly this is an experimental feature and might require a custom build from the source code.
There is reference to such functionality on the GitHub pages but I don't know if I have to compile a build with this source:
https://github.com/openlayers/ol3/blob/5c5364bbb7e8df76f18242ad665c87ca08a76e76/src/ol/source/bingmapssource.js
Can anyone provide an example of this type of conversion or indeed does anyone know if the latest (3.8.2) version of OL3 supports the quadkey method?
This is the leaflet example:
var BingLayer = L.TileLayer.extend({
getTileUrl: function (tilePoint) {
this._adjustTilePoint(tilePoint);
return L.Util.template(this._url, {
s: this._getSubdomain(tilePoint),
q: this._quadKey(tilePoint.x, tilePoint.y, this._getZoomForUrl())
});
},
_quadKey: function (x, y, z) {
var quadKey = [];
for (var i = z; i > 0; i--) {
var digit = '0';
var mask = 1 << (i - 1);
if ((x & mask) != 0) {
digit++;
}
if ((y & mask) != 0) {
digit++;
digit++;
}
quadKey.push(digit);
}
return quadKey.join('');
}
});
And this is the exisiting Silverlight code:
public override Uri GetUri(int x, int y, int zoomLevel, bool getPrintLink)
{
Uri uri = null;
if (this.Covers(x, y, zoomLevel))
{
QuadKey qk = new QuadKey(x, y, zoomLevel);
if (getPrintLink)
{
uri = new Uri(this.CurrentHostURL + "/tiles/NL/" + zoomLevel.ToString() + "/" + qk.Key + ".ipic", UriKind.RelativeOrAbsolute);
}
else
{
uri = new Uri("http://tileserver.satmap.com/NL/" + zoomLevel.ToString() + "/" + qk.Key + ".ipic", UriKind.RelativeOrAbsolute);
}
}
return uri;
}
Any insight would be appreciated as I've trawled many forums and countless pages of search results without finding a solution.
As far as I can see, your _quadKey function is correct. The issue that might arise is understanding the ol.TileCoord provided to the URL function.
In OpenLayers 3.7 and later, all TileCoords are calculated with the top left corner as their origin. Furthermore, both the X and Y coordinates increase naturally, so that the X and Y coordinates of a TileCoord corresponds to the normal concepts of a two-dimensional axis.
The top left tile of a given zoom level will always have X=0 and Y=-1. The tile below that will have X=0 and Y=-2; Y will always be negative.
Some mapping applications, such as Bing, also use the top left corner as the tile origin, but lets the Y coordinate increase downwards. The top left tile would the be X=0 and Y=0, while the tile below would be X=0 and Y=1.
In order to calculate the quadkeys, the Y coordinate has to be inverted and adjusted by one. This should work:
// this is unchanged from the question
var quadkey = function (x, y, z) {
var quadKey = [];
for (var i = z; i > 0; i--) {
var digit = '0';
var mask = 1 << (i - 1);
if ((x & mask) != 0) {
digit++;
}
if ((y & mask) != 0) {
digit++;
digit++;
}
quadKey.push(digit);
}
return quadKey.join('');
};
var quadKeyLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
maxZoom: 19,
tileUrlFunction: function (tileCoord, pixelRatio, projection) {
var z = tileCoord[0];
var x = tileCoord[1];
var y = -tileCoord[2] - 1;
return "//example.com/r" + quadkey(x, y, z);
}
})
});

How to rotate/transform mapbox-gl-draw features?

I'm using mapbox-gl-draw to add move-able features to my map. In addition to movability functionality, I am needing rotate/transform -ability functionality for the features akin to Leaflet.Path.Transform.
At current, would my only option to achieve be to create a custom mode?
e.g. something like:
map.on('load', function() {
Draw.changeMode('transform');
});
I am not able to convert my map and it's features to mapbox-gl-leaflet in order to implement Leaflet.Path.Transform as losing rotation / bearing / pitch support is not an option.
Long answer incoming. (see http://mapster.me/mapbox-gl-draw-rotate-mode and http://npmjs.com/package/mapbox-gl-draw-rotate-mode for some final products, https://github.com/mapstertech/mapbox-gl-draw-rotate-mode)
I've been working on something similar for a custom project, and not using a draw library. My project involves some pretty regularly sized objects, not very complex polygons, so the solution might be too simple for you but it may be the right path. I just have rotate and move.
Doing movement isn't too hard geographically. Here's some help to get you started. A basic JSBin is up at https://jsbin.com/yoropolewo/edit?html,output with some drag functionality (too tired to do rotate too).
First, register the necessary click events to have a dragging event. You can listen on the specific Mapbox layers for a mousedown, then on the whole document for a mousemove and mouseup.
To do individual shape rotation, you need to ensure that you are referring to the right feature. In this example I assume there's just one feature in the source data, but that's probably too simple for most uses, so you have to extrapolate. The source data is what we affect when we setData() later on. There are obviously numerous ways to do what I'm doing here, but I'm trying to be clear.
var currentDragging = false;
var currentDraggingFeature = false;
var currentDraggingType = false;
var firstDragEvent = false;
map.on('mousedown','my-layer-id',function(e) {
currentDragging = 'my-source-id'; // this must correspond to the source-id of the layer
currentDraggingFeature = e.features[0]; // you may have to filter this to make sure it's the right feature
currentDraggingType = 'move'; // rotation or move
firstDragEvent = map.unproject([e.originalEvent.layerX,e.originalEvent.layerY]);
});
window.addEventListener('mousemove',dragEvent);
window.addEventListener('mouseup',mouseUpEvent);
You will need a function, then, that takes an initial point, a distance, and a rotation, and returns a point back to you. Like this:
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() {
return this * 180 / Math.PI;
}
function getPoint(point, brng, dist) {
dist = dist / 63.78137; // this number depends on how you calculate the distance
brng = brng.toRad();
var lat1 = point.lat.toRad(), lon1 = point.lng.toRad();
var lat2 = Math.asin(Math.sin(lat1) * Math.cos(dist) +
Math.cos(lat1) * Math.sin(dist) * Math.cos(brng));
var lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dist) *
Math.cos(lat1),
Math.cos(dist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return [lon2.toDeg(),lat2.toDeg()];
}
Now, the key is the unproject method in Mapbox GL JS, so you can move between x/y coordinates on the mouse and lng/lat on your map. Then, using the map.getSource().setData() function to set a new geoJSON.
I am turning the x/y into coordinates immediately here but you can do it at any point. Something like the following for moving:
function moveEvent(e) {
// In the case of move, you are just translating the points based on distance and angle of the drag
// Exactly how your translate your points here can depend on the shape
var geoPoint = map.unproject([e.layerX,e.layerY]);
var xDrag = firstDragEvent.lng - geoPoint.lng;
var yDrag = firstDragEvent.lat - geoPoint.lat;
var distanceDrag = Math.sqrt( xDrag*xDrag + yDrag*yDrag );
var angle = Math.atan2(xDrag, yDrag) * 180 / Math.PI;
// Once you have this information, you loop over the coordinate points you have and use a function to find a new point for each
var newFeature = JSON.parse(JSON.stringify(currentDraggingFeature));
if(newFeature.geometry.type==='Polygon') {
var newCoordinates = [];
newFeature.geometry.coordinates.forEach(function(coords) {
newCoordinates.push(getPoint(coords,distanceDrag,angle));
});
newFeature.geometry.coordinates = newCoordinates;
}
map.getSource(currentDragging).setData(newFeature);
}
Rotating is a little harder because you want the shape to rotate around a central point, and you need to know the distance of each point to that central point in order to do that. If you have a simple square polygon this calculation would be easy. If not, then using something like this would be helpful (Finding the center of Leaflet polygon?):
var getCentroid2 = function (arr) {
var twoTimesSignedArea = 0;
var cxTimes6SignedArea = 0;
var cyTimes6SignedArea = 0;
var length = arr.length
var x = function (i) { return arr[i % length][0] };
var y = function (i) { return arr[i % length][1] };
for ( var i = 0; i < arr.length; i++) {
var twoSA = x(i)*y(i+1) - x(i+1)*y(i);
twoTimesSignedArea += twoSA;
cxTimes6SignedArea += (x(i) + x(i+1)) * twoSA;
cyTimes6SignedArea += (y(i) + y(i+1)) * twoSA;
}
var sixSignedArea = 3 * twoTimesSignedArea;
return [ cxTimes6SignedArea / sixSignedArea, cyTimes6SignedArea / sixSignedArea];
}
Once you have the ability to know the polygon's center, you're golden:
function rotateEvent(e) {
// In the case of rotate, we are keeping the same distance from the center but changing the angle
var findPolygonCenter = findCenter(currentDraggingFeature);
var geoPoint = map.unproject([e.layerX,e.layerY]);
var xDistanceFromCenter = findPolygonCenter.lng - geoPoint.lng;
var yDistanceFromCenter = findPolygonCenter.lat - geoPoint.lat;
var angle = Math.atan2(xDistanceFromCenter, yDistanceFromCenter) * 180 / Math.PI;
var newFeature = JSON.parse(JSON.stringify(currentDraggingFeature));
if(newFeature.geometry.type==='Polygon') {
var newCoordinates = [];
newFeature.geometry.coordinates.forEach(function(coords) {
var xDist = findPolygonCenter.lng - coords[0];
var yDist = findPolygonCenter.lat - coords[1];
var distanceFromCenter = Math.sqrt( xDist*xDist + yDist*yDist );
var rotationFromCenter = Math.atan2(xDist, yDist) * 180 / Math.PI;
newCoordinates.push(
getPoint(coords,distanceFromCenter,rotationFromCenter+angle)
);
});
newFeature.geometry.coordinates = newCoordinates;
}
}
Of course, throughout, ensure that your coordinates are being passed and returned correctly from functions. Some of this code may have incorrect levels of arrays in it. It's very easy to run into bugs with the lat/lng object versus the geoJSON arrays.
I hope the explanation is brief but clear enough, and that you understand logically what we are doing to reorient these points. That's the main point, the exact code is details.
Maybe I should just make a module or fork GL Draw...