Back

Travel planner with GeoChart and NetChart

Interactive JavaScript travel planner that consists of a map chart with a network chart as an additional layer on top of the world map. Click on a node to pick a starting point, and then hover over your destination to see the best travel routes.

Shows shortest distance, least flight segments or both. On links shows
Documentation Open in JSFiddle
Start Free Trial Purchase

HTML

HTML
<script src="https://cdn.zoomcharts-cloud.com/1/nightly/zoomcharts.js"></script>

    Shows <span style="border-bottom: 2px solid rgba(0,153,204,0.9)">shortest distance</span>, <span style="border-bottom: 2px solid rgba(213,66,155,1)">least flight segments</span>
    or <span style="border-bottom: 2px solid rgba(119,114,181,1)">both</span>.
    On links shows <select id="link_chooser">
        <option value="distance" selected="selected">distance</option>
        <option value="duration">duration</option>
    </select>

    <div id="demo"></div>

CSS

CSS
//No CSS for this example 

JavaScript

JavaScript

    var pathColorDistance = "rgba(0,153,204,0.9)";
    var pathColorSegments = "rgba(213,66,155,1)";
    var pathColorBoth = "rgba(119,114,181,1)";

    // the current selected path, contains both the node and link IDs
    var startNodeId = "BWI"; //null;
    var endNodeId = "GOT"; //null;
    var pathData = null;

    // to improve performance, cache the last calculated path
    var lastEndNodeId = "BWI"; //null;
    var lastPathData = "GOT"; //null;

    // first click enables hover functionality:
    var initialClick = null;

    function computePath() {
        // BFS implementation to find the shortest paths
        
        var from = chart.getNode(startNodeId);
        var back = {};
        back[from.id] = { distance: 0, segments: 0, segmentDistance: 0, previousDistance: null, previousSegments: null };
        var queue = [from];
        var link, next, cur;

        while (queue.length > 0) {
            cur = queue.pop();

            for (var i = 0; i < cur.links.length; i++) {
                link = cur.links[i];
                next = link.otherEnd(cur);

                var curResult = back[cur.id];
                var nextResult = back[next.id];
                var visitNext = false;

                var targetDistance = curResult.distance + link.data.distance_km;
                var targetSegments = curResult.segments + 1;

                // do not got too deep
                if (targetSegments === 6)
                    continue;

                if (nextResult === void 0) {
                    back[next.id] = {
                        distance: targetDistance,
                        segments: targetSegments,
                        segmentDistance: targetDistance,
                        previousDistance: link,
                        previousSegments: link
                    };
                    visitNext = true;
                } else {
                    if (nextResult.distance > targetDistance) {
                        nextResult.distance = targetDistance;
                        nextResult.previousDistance = link;
                        visitNext = true;
                    }
                    if (nextResult.segments > targetSegments || (nextResult.segments === targetSegments && nextResult.segmentDistance > targetDistance)) {
                        nextResult.segments = targetSegments;
                        nextResult.segmentDistance = targetDistance;
                        nextResult.previousSegments = link;
                        visitNext = true;
                    }
                }

                if (visitNext)
                    queue.push(next);
            }
        }

        // walk back from the target node to the start
        var pathData = back[endNodeId];
        if (pathData === void 0) {
            return null;
        }

        var resultDistance = {};
        var cur = chart.getNode(endNodeId);
        while (true) {
            var link = pathData.previousDistance;

            // found the first node
            if (link === null)
                break;

            cur = link.otherEnd(cur);
            resultDistance[link.id] = true;
            resultDistance[cur.id] = true;

            pathData = back[cur.id];
        }

        var resultSegments = {};
        var pathData = back[endNodeId];
        var cur = chart.getNode(endNodeId);
        while (true) {
            var link = pathData.previousSegments;

            // found the first node
            if (link === null)
                break;

            cur = link.otherEnd(cur);
            resultSegments[link.id] = true;
            resultSegments[cur.id] = true;

            pathData = back[cur.id];
        }
        return { distance: resultDistance, segments: resultSegments };
    }

    function recalculatePathStyle() {
        if (!pathData)
            return;

        var idArray = [startNodeId, endNodeId];

        for (var key in pathData.distance)
            idArray.push(key);
        for (var key in pathData.segments)
            idArray.push(key);

        // only recalculate the style for the affected nodes and links to improve performance
        chart.updateStyle(idArray);
    }

    var link_chooser = document.getElementById("link_chooser");
    var options = {
        area: { height: null },
        container: "demo",
        data: { url: "/dvsl/data/geo-chart/airports.json" },
        layers: [
            {
                name: "Points",
                type: "items",
                style: {
                    scaleObjectsWithZoom: true,
                    scaleLinksWithZoom: false,
                    link: {
                        invisible: true,
                        radius: 4
                    },
                    linkLabel: {
                        padding: 1,
                        borderRadius: 3,
                        textStyle: { font: "10px Arial", fillColor: "#fff" }
                    },
                    node: {
                        radius: 0.7,
                        fillColor: "rgba(0, 0, 0, 0.5)",
                        lineColor: "rgba(255, 255, 255, 0)"
                    },
                    nodeLabel: {
                        textStyle: { font: "11px Arial", fillColor: "#fff" }
                    },
                    nodeStyleFunction: function (node) {
                        node.label = "";
                        if (node.id == startNodeId || node.id == endNodeId) {
                            node.label = node.data.name;
                            node.radius = 1;
                            node.fillColor = "rgba(0, 0, 0, 0.9)";
                            node.labelStyle.backgroundStyle.fillColor = "rgba(0, 0, 0, 0.9)";
                            node.labelStyle.padding = 2;
                            node.labelStyle.borderRadius = 3;
                        } else if (pathData) {
                            var isPartOfShortestDistance = pathData.distance[node.id];
                            var isPartOfShortestSegments = pathData.segments[node.id];

                            if (isPartOfShortestDistance || isPartOfShortestSegments) {
                                node.label = node.data.name;
                                node.labelStyle.backgroundStyle.fillColor = isPartOfShortestDistance ? isPartOfShortestSegments ? pathColorBoth : pathColorDistance : pathColorSegments;
                            }
                        }
                    },
                    linkStyleFunction: function (link) {
                        var isPartOfShortestDistance = pathData && pathData.distance[link.id];
                        var isPartOfShortestSegments = pathData && pathData.segments[link.id];
                        if (isPartOfShortestDistance || isPartOfShortestSegments) {
                            link.invisible = false;

                            var fillColor = isPartOfShortestDistance ? isPartOfShortestSegments ? pathColorBoth : pathColorDistance : pathColorSegments;
                            link.fillColor = fillColor;
                            link.labelStyle.backgroundStyle.fillColor = fillColor;

                            if (link_chooser.selectedIndex === 1 /*duration*/) {
                                var duration = parseFloat(link.data.duration_km);
                                var duration_hours = Math.floor(duration);
                                var duration_minutes = (duration - duration_hours) * 60;
                                link.label = duration_hours > 0 ? duration_hours + 'h ' : '';
                                link.label += duration_minutes > 0 ? Math.ceil(duration_minutes) + 'min' : '';
                            } else {
                                link.label = link.data.distance_km + "\u00A0km";
                            }
                        }
                    }
                }
            }
        ],
        events: {
            onHoverChange: function (event) {
                //if at least one click has been made, enable hover functionality
                if(!initialClick) {
                    return;
                }

                if (startNodeId && event.hoverNode && startNodeId !== event.hoverNode.id) {
                    // the style for the currently selected nodes and links have to be recalculated
                    recalculatePathStyle();

                    endNodeId = event.hoverNode.id;
                    if (lastEndNodeId !== endNodeId) {
                        pathData = computePath();

                        // the hoverChange event might be called even if the node does not change (for example the label is hovered)
                        // and also it is likely that the user might hover the same node multiple times.
                        // thus the calculated path is cached.
                        lastEndNodeId = endNodeId;
                        lastPathData = pathData;
                    } else {
                        pathData = lastPathData;
                    }

                    // the style for the newly selected nodes and links have to be recalculated
                    recalculatePathStyle();
                } else if (endNodeId) {
                    // the style for the currently selected nodes and links have to be recalculated
                    recalculatePathStyle();

                    endNodeId = null;
                    pathData = null;
                }
            },
            onClick: function (event) {
                initialClick = true;
                if (event.clickNode) {
                    // the style for the start node must be recalculated
                    chart.updateStyle([startNodeId, event.clickNode.id]);

                    startNodeId = event.clickNode.id;
                }
            }
        },
    };

    chart = new GeoChart(options);
    setTimeout(function() {
        pathData = computePath();
        recalculatePathStyle();
    },3000);

Data

Data
//Data too large to output
Download Data