Maps API: Finding the longest common path in two given paths - algorithm

Google maps and Bing maps have methods that can give the directions from point A to point B on a map. This highlights a path from A to B on the map - call this P1
Suppose, P2 is another path from C to D (some other points), how can we find the longest common length of path between paths P1 and P2?

You have plenty of ways to do what you want.
Curiously, I tried to do it using JavaScript only and to do so, I used JSTS library that would compute the intersection between two routes (in my case, the geometry were retrieved from Bing, but I did not include the request in this example as it's not helpful).
Use case:
So, you want to have the common path between two paths (or the part of a route where you can use car-sharing or where you can run with your friend for example), if this is correct, then this example will help you.
Libraries:
First, the following library is need: JSTS, you can get it through Github dedicated repository: https://github.com/bjornharrtell/jsts
On other interesting library is Turf available here: https://github.com/Turfjs/
Implementation with JSTS and leaflet:
Here is the piece of JavaScript that will be interesting in this case:
<script type="text/javascript">
var routeCoordinatesA = [[50.619512, 3.061242]....TRUNCATED FOR READIBILITY** ];
var routeCoordinatesB = [[50.619512, 3.061242]....TRUNCATED FOR READIBILITY** ];
$(function () {
var map = L.map('map').setView([47.5, 2.75], 5);
// Add base tile layer - sample from Leaflet website
L.tileLayer('http://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var polylineA = L.polyline(routeCoordinatesA, { color: '#4b98dc' }).addTo(map);
var polylineB = L.polyline(routeCoordinatesB, { color: '#de6262' }).addTo(map);
var geometryFactory = new jsts.geom.GeometryFactory();
// Coordinates adapted to match for jsts
var coordsA = [];
$.each(routeCoordinatesA, function (idx, current) { coordsA.push([current[1], current[0]]); });
var coordsB = [];
$.each(routeCoordinatesB, function (idx, current) { coordsB.push([current[1], current[0]]); });
// Element A
var coordinatesA = bindCoord2JTS(coordsA);
var shellA = geometryFactory.createLinearRing(coordinatesA);
var jstsPolygonA = geometryFactory.createPolygon(shellA);
// Element b
var coordinatesB = bindCoord2JTS(coordsB);
var shellB = geometryFactory.createLinearRing(coordinatesB);
var jstsPolygonB = geometryFactory.createPolygon(shellB);
// Interection
var bufferTolerance = (2 / 1000); // Small buffer to avoid different node no detection
var intersection = shellA.buffer(bufferTolerance).intersection(shellB);
var intersectionPoints = [];
$.each(intersection.getCoordinates(), function (idx, current) {
intersectionPoints.push([current.x, current.y]);
});
intersectionPoints.pop();
var intersectionLine = L.polyline(intersectionPoints, { color: '#4fc281', weight: 8 }).addTo(map);
map.fitBounds(routeCoordinatesA.concat(routeCoordinatesB));
});
var bindCoord2JTS = function (coords) {
var coordinates = [];
for (var i = 0; i < coords.length; i++) {
coordinates.push(new jsts.geom.Coordinate(
coords[i][1], coords[i][0]));
}
return coordinates;
};
You can grab all the working example among my Leaflet experiments available on Github as well:
https://github.com/nicoboo/maps/tree/master
And here the page that implements what I was talking about:
https://github.com/nicoboo/maps/blob/master/Boo.Maps.Web.LeafletExperiments/LeafletWithin/index.html
Here for the live demo: http://htmlpreview.github.io/?https://github.com/nicoboo/maps/blob/master/Boo.Maps.Web.LeafletExperiments/LeafletWithin/index.html
Considerations:
Of course, this is really based on the client side and it might be usefull to have the information on the server-side, I would recommend to use a spatially enabled database so you can use the STBuffer() and STIntersection() methods directly on the column or results that you manipulate with the best performances.

I am not sure to fully understand your request but both Bing maps ans Google maps API for the directions contains in their response a "distance" field which specifies the value of the directions.
Here are two links for both documentation:
Bing Maps & Google Maps
With that you could compare the distance value between the two path and find the longest.
Hope this help.

Related

AmCharts AmMap - Set starting location for zoom actions

I would like to use the "zoomToMapObject" method based on a selection on a dropdown menu.
For some reason the start zoom location is the middle of the map and not the set the geoPoint.
(The zooming works but the start location make it look a bit weird.)
My current approach looks like this:
const duration = this.chart.zoomToMapObject(selectedPoloygon, this.countryZoom, true).duration;
setTimeout(() => {
this.chart.homeGeoPoint = geoPoint;
this.chart.homeZoomLevel = this.countryZoom;
}, duration);
this.handleCountrySelection(selectedPoloygon);
Somehow even setting the homeGeoPoint / homeZoomLevel doesn't affect next zoom actions.
**UPDATE: Workaround heavy cost (from 1300 nodes to over 9000) **
I examined the problem a step further. It seems the middle point gets set when I push a new mapImageSeries into the map.
My workarround currently is to draw all points on the map and hide them.
Then after I select a country I change the state to visible.
However this approach is very costly. The DOM-Nodes rises from 1300 to ~ 9100.
My other approach with creating them after a country has been selected AND the zoom animation finished was much more
effective. But due to the map starting every time for a center location it is not viable? Or did I do s.th. wrong?
Here is my current code which is not performant:
// map.ts
export class MapComponent implements AfterViewInit, OnDestroy {
imageSeriesMap = {};
// ... standard map initialization ( not in zone of course )
// creating the "MapImages" which is very costly
this.dataService.getCountries().forEach(country => {
const imageSeriesKey = country.id;
const imageSeriesVal = chart.series.push(new am4maps.MapImageSeries()); // takes arround 1-2 ms -> 300 x 2 ~ 500 ms.
const addressForCountry = this.dataService.filterAddressToCountry(country.id); // returns "DE" or "FR" for example.
const imageSeriesTemplate = imageSeriesVal.mapImages.template;
const circle = imageSeriesTemplate.createChild(am4core.Circle);
circle.radius = 4;
circle.fill = am4core.color(this.colorRed);
circle.stroke = am4core.color('#FFFFFF');
circle.strokeWidth = 2;
circle.nonScaling = true;
circle.tooltipText = '{title}';
imageSeriesTemplate.propertyFields.latitude = 'latitude';
imageSeriesTemplate.propertyFields.longitude = 'longitude';
imageSeriesVal.data = addressForCountry.map(address => {
return {
latitude: Number.parseFloat(address.lat),
longitude: Number.parseFloat(address.long),
title: address.company
};
});
imageSeriesVal.visible = false;
this.imageSeriesMap[imageSeriesKey] = imageSeriesVal;
});
// clicking on the map
onSelect(country) {
this.imageSeriesMap[country].visible = true;
setTimeout( () => {
const chartPolygons = <any>this.chart.series.values[0];
const polygon = chartPolygons.getPolygonById(country);
const anim = this.chart.zoomToMapObject(polygon, 1, true, 1000);
anim.events.on('animationended', () => {});
this.handleCountrySelection(polygon);
}, 100);
});
}
handleCountrySelection(polygon: am4maps.MapPolygon) {
if (this.selectedPolygon && this.selectedPolygon !== polygon) {
this.selectedPolygon.isActive = false;
}
polygon.isActive = true;
const geoPoint: IGeoPoint = {
latitude: polygon.latitude,
longitude: polygon.longitude
};
this.chart.homeGeoPoint = geoPoint;
this.chart.homeZoomLevel = this.countryZoom;
this.selectedPolygon = polygon;
}
}
Thanks to your thorough followup I was able to replicate the issue. The problem you were having is triggered by any one of these steps:
dynamically pushing a MapImageSeries to the chart
dynamically creating a MapImage via data (also please note in the pastebind you provided, data expects an array, I had to change that while testing)
In either step, the chart will fully zoom out as if resetting itself. I'm going to look into why this is happening and if it can be changed, so in the meantime let's see if the workaround below will work for you.
If we only use a single MapImageSeries set in advance (I don't particularly see a reason to have multiple MapImageSeries, would one not do?), that eliminates problem 1 from occurring. Asides from data, we can create() MapImages manually via mapImageSeries.mapImages.create(); then assign their latitude and longitude properties manually, too. With that, problem 2 does not occur either, and we seem to be good.
Here's a demo with a modified version of the pastebin:
https://codepen.io/team/amcharts/pen/c460241b0efe9c8f6ab1746f44d666af
The changes are that the MapImageSeries code is taken out of the createMarkers function so it only happens once:
const mapImageSeries = chart.series.push(new am4maps.MapImageSeries());
const imageSeriesTemplate = mapImageSeries.mapImages.template;
const circle = imageSeriesTemplate.createChild(am4core.Circle);
circle.radius = 10;
circle.fill = am4core.color('#ff0000');
circle.stroke = am4core.color('#FFFFFF');
circle.strokeWidth = 2;
circle.nonScaling = true;
circle.tooltipText = 'hi';
In this case, there's no need to pass chart to createMarkers and return it, so I've passed polygon instead just to demo dynamic latitude/longitudes, I also assign our new MapImage to the polygon's data (dataItem.dataContext) so we can refer to it later. Here's the new body of createMarkers:
function createMarkers(polygon) {
console.log('calling createMarkers');
if ( !polygon.dataItem.dataContext.redDot) {
const dataItem = polygon.dataItem;
// Object notation for making a MapImage
const redDot = mapImageSeries.mapImages.create();
// Note the lat/long are direct properties
redDot.id = `reddot-${dataItem.dataContext.id}`;
// attempt to make a marker in the middle of the country (note how this is inaccurate for US since we're getting the center for a rectangle, but it's not a rectangle)
redDot.latitude = dataItem.north - (dataItem.north - dataItem.south)/2;
redDot.longitude = dataItem.west - (dataItem.west - dataItem.east)/2;;
dataItem.dataContext.redDot = redDot;
}
}
There's no need for the animationended event or anything, it just works since there is no longer anything interfering with your code. You should also have your performance back.
Will this work for you?
Original answer prior to question's edits below:
I am unable to replicate the behavior you mentioned. Also, I don't know what this.countryZoom is.
Just using the following in a button handler...
chart.zoomToMapObject(polygon);
...seems to zoom just fine to the country, regardless of the current map position/zoomLevel.
If you need to time something after the zoom animation has ended, the zoomToMapObject returns an Animation, you can use its 'animationended' event, e.g.
const animation = this.chart.zoomToMapObject(selectedPoloygon, this.countryZoom, true);
animation.events.on("animationended", () => {
// ...
});
Here's an example with all that with 2 external <button>s, one for zooming to USA and the other Brazil:
https://codepen.io/team/amcharts/pen/c1d1151803799c3d8f51afed0c6eb61d
Does this help? If not, could you possibly provide a minimal example so we can replicate the issue you're having?

D3 stack() vs nested objects

I'm running into an issue when trying to implement a normalized stacked bar chart using D3v4.
The problem occurs due to my data format which contains nested object arrays populated dynamically on the server side.
var data = [{x:"data1", y:[{name:"red", value:10}, {name:"green", value:20}]},
{x:"data2", y:[{name:"red", value:30}, {name:"green", value:5}]}];
Calling d3.stack() on this will not work since d3 doesn't know how to traverse into the object array y. (https://jsfiddle.net/xv1qgqjg/)
Is there any way to tell d3.stack() where to find the relevant data similar to the .data(function(d){ return d.y; }) used elsewhere?
It doesn't seem to be possible. According to the documentation regarding stack(data[, arguments…]),
Any additional arguments are arbitrary; they are simply propagated to accessors along with the this object.
Thus, you'll have to change your data, creating an array which you can pass to d3.stack(), such as this:
[{red:10,green:20},
{red:30,green:5}]
Given the data array in your question, there are several ways for creating the above-mentioned array. Here is my solution (the new array is called newData):
newData = [];
data.forEach(d => {
var tempObj = {}
d.y.forEach(e => {
tempObj[e.name] = e.value;
})
newData.push(tempObj);
});
Here is a demo:
var data = [{x:"data1", y:[{name:"red", value:10}, {name:"green", value:20}]},
{x:"data2", y:[{name:"red", value:30}, {name:"green", value:5}]}];
newData = [];
data.forEach(d => {
var tempObj = {}
d.y.forEach(e => {
tempObj[e.name] = e.value;
})
newData.push(tempObj);
});
var stack = d3.stack()
.keys(["red", "green"])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetExpand);
var series = stack(newData);
console.dir(series);
<script src="https://d3js.org/d3.v4.min.js"></script>

Get all dom nodes from d3 selection

selection.node() returns only the first node. Can we get an array of all nodes from a selection?
EDIT Added some code to help us.
The attempt with each() is the only one producing the wanted
output, although quite verbose.
Calling sel[0] also returns an array with DOM nodes, but it's hacky (depends on the internal structure of the library) and includes an unwanted "parentNode" field.
// creating a selection to experiment with
var data= [1,2,3,4]
var sel = d3.select("li")
.data(data)
.enter().append("li").html(identity);
function identity(d){return d}
console.log(sel); // array[1] with array[4] with the <li>'s
// using .node()
var res1 = sel.node();
console.log(res1); // first <li> only
// using .each() to accumulate nodes in an array
var res2 = [];
function appendToRes2(){
res2.push(this);
}
sel.each(appendToRes2);
console.log(res2); // array[4] with the <li>'s (what I want)
// calling sel[0]
var res3 = sel[0];
console.log(res3); // array[4] with the <li>'s plus a "parentNode"
// #thisOneGuy's suggestion
var res4 = d3.selectAll(sel);
console.log(res4); // array[1] with array[1] with array[4] with the <li>'s
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
EDIT 2 Why do I want to do that?
To call array methods like reduce and map on the DOM nodes. D3 provides filter but to use others I first need to extract the node array from the selection.
I originally wrote this as a comment, but decided to turn it into an answer...
It looks like d3 v4 will include the functionality you want. If you don't want to wait, you can steal the implementation now and add it to the selection prototype:
d3.selection.prototype.nodes = function(){
var nodes = new Array(this.size()), i = -1;
this.each(function() { nodes[++i] = this; });
return nodes;
}
Usage example:
d3.selection.prototype.nodes = function(){
var nodes = new Array(this.size()), i = -1;
this.each(function() { nodes[++i] = this; });
return nodes;
}
var data= [1,2,3,4]
var sel = d3.select("li")
.data(data)
.enter().append("li").html(identity);
function identity(d){return d}
console.log(sel.nodes());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Since it came from #mbostock, it's a good bet is the best implementation available.

Export crossfilter dataset to excel in dc.js

I made a visualization page using crossfilter.js and dc.js . I want to export the filtered dataset to excel. Is any way to do this.?
I think the best way to do this is to create another dimension and then call dimension.top(Infinity) to get all the records (sorted by that dimension's key).
Jacob Rideout created a pull request for a new method to do just this without the overhead, but it was not accepted (doesn't look like it was rejected either ;):
https://github.com/square/crossfilter/pull/95
But I doubt you will notice any performance penalty for creating the extra dimension. (Please comment on that PR if you do!)
function groupArrayAdd(keyfn) {
var bisect = d3.bisector(keyfn);
return function (elements, item) {
var pos = bisect.right(elements, keyfn(item));
elements.splice(pos, 0, item);
return elements;
};
}
function groupArrayRemove(keyfn) {
var bisect = d3.bisector(keyfn);
return function (elements, item) {
var pos = bisect.left(elements, keyfn(item));
if (keyfn(elements[pos]) === keyfn(item))
elements.splice(pos, 1);
return elements;
};
}
function groupArrayInit() {
return [];
}
var facts = crossfilter(data); //pass your mater dataset here.
var filteredRows = facts.groupAll().reduce(
groupArrayAdd(dc.pluck('shift')),
groupArrayRemove(dc.pluck('shift')),
groupArrayInit}
);
filteredRows.value() will give you the crossfilted data. Every time the data is filteded, this function will give automatically five the filted output which you can use to export to excel using any jquery plugin.
Another way to find out filtered data is using below dc function:
dimension.top(Infinity)

Extending dc.js to add a "simpleLineChart" chart

edit See here for the non-working example of what I'm trying to do: http://bl.ocks.org/elsherbini/5814788
I am using dc.js to plot data collected from bee hives at my university. I am pushing new data to the graphs on every database change (using the magic of Meteor). When the database is over 5000 records or so, rerendering the lines gets really slow. So I want to use simplify.js to preprocess the lines before rendering. To see what I'm talking about, go to http://datacomb.meteor.com/. The page freezes after a couple of seconds, so be warned.
I have started to extend dc.js with a simpleLineChart, which would inherit from the existing dc.lineChart object/function. Here is what I have so far:
dc.simpleLineChart = function(parent, chartGroup) {
var _chart = dc.lineChart(),
_tolerance = 1,
_highQuality = false,
_helperDataArray;
_chart.tolerance = function (_) {
if (!arguments.length) return _tolerance;
_tolerance = _;
return _chart;
};
_chart.highQuality = function (_) {
if (!arguments.length) return _highQuality;
_highQuality = _;
return _chart;
};
return _chart.anchor(parent, chartGroup);
}
simplify.js takes in an array of data, a tolerance, and a boolean highQuality, and returns a new array with fewer elements based on it's simplification algorithm.
dc.js uses crossfilter.js. dc.js charts are associated with a particular crossfilter dimension and group. Eventually, it uses the data from someGroup().all() as the data to pass to a d3.svg.line(). I can't find where this is happening in the dc.js source, but this is where I need to intervene. I want to find this method, and override it in the dc.simpleLineChart object that I am making.
I was thinking something like
_chart.theMethodINeedToOverride = function(){
var helperDataArray = theChartGroup().all().map(function(d) { return {
x: _chart.keyAccessor()(d),
y: _chart.valueAccessor()(d)};})
var simplifiedData = simplify(helperDataArray, _tolerance, _highQuality)
g.datum(simplifiedData); // I know I'm binding some data at some point
// I'm just not sure to what or when
}
Can anyone help me either identify which method I need to override, or even better, show me how to do so?
dc.js source: https://github.com/NickQiZhu/dc.js/blob/master/dc.js
edit:
I think I may have found the function I need to override. The original function is
function createGrouping(stackedCssClass, group) {
var g = _chart.chartBodyG().select("g." + stackedCssClass);
if (g.empty())
g = _chart.chartBodyG().append("g").attr("class", stackedCssClass);
g.datum(group.all());
return g;
}
And I have tried to override it like so
function createGrouping(stackedCssClass, group) {
var g = _chart.chartBodyG().select("g." + stackedCssClass);
if (g.empty())
g = _chart.chartBodyG().append("g").attr("class", stackedCssClass);
var helperDataArray = group().all().map(function(d) { return {
x: _chart.keyAccessor()(d),
y: _chart.valueAccessor()(d)};})
var simplifiedData = simplify(helperDataArray, _tolerance, _highQuality)
g.datum(simplifiedData);
return g;
}
However, when I make a simpleLineChart, it is just a linechart with a tolerance() and highQuality() method. See here: http://bl.ocks.org/elsherbini/5814788
Well, I pretty much did what I set out to do.
http://bl.ocks.org/elsherbini/5814788
The key was to not only modify the createGrouping function, but also the lineY function in the code. (lineY gets set to tell the d3.svg.line() instance how to set the y value of a given point d)
I changed it to
var lineY = function(d, dataIndex, groupIndex) {
return _chart.y()(_chart.valueAccessor()(d));
};
The way lineY was written before, it was looking up the y value in an array, rather than using the data bound to the group element. This array had it's data set before i made my changes, so it was still using the old, pre-simplification data.

Resources