A* search algorithm（グラフ探索）

こんにちは。
[A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm#Variants_of_A.2A) の Javascript 実装（下記）を利用し、最短経路探索を行ってみました[^1] [^2]。地図表示には [d3.mapzoom](http://bl.ocks.org/sdjacobs) を利用しました[^3]。

- [A* Search Algorithm in JavaScript (Updated)](http://www.briangrinstead.com/blog/astar-search-algorithm-in-javascript-updated) - Brian Grinstead
 - [javascript-astar (demo)] (http://bgrins.github.io/javascript-astar/demo)
 - https://github.com/bgrins/javascript-astar

始点は Paris、終点は Cannes の条件（```query = {"start": "Paris", "end": "Cannes"};```）を与えると下記の結果（および計算時間、最短経路解は地図上で矢印付き表示）が得られます。
[^1]: [pathfinding](https://en.wikipedia.org/wiki/Pathfinding) (wikipedia) or route finding。
[^2]: 「[AStar.js(A*アルゴリズム)を使ってみた](http://qiita.com/YFukuyama/items/bd11950d59f7680bee53)」
[^3]: 表示にMapboxGLを利用した版は「[A* search algorithm（グラフ探索、MapboxGL）](http://qiita.com/kkdd/items/21cdc9611fd7e85f75ec#_reference-0f2d4e6f01a65575c77c])」。


<img width="457" alt="astar.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/54617/f6a9a67f-1934-a514-7b0a-37331940ae2d.jpeg">

```astar.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta http-equiv="content-language" content="ja">
<title>A-star</title>
<!-- A* (A-star) route finding on a zoomable map using javascript-astar (astar.js) by Brian Grinstead and d3.mapzoom by Simon Jacobs
- astar.js (https://github.com/bgrins/javascript-astar)
- d3.mapzoom.js (http://bl.ocks.org/sdjacobs) -->
<script src="astar.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.tile.v0.min.js"></script>
<script src="d3.mapzoom.js"></script>
<style>
circle {
  fill: #ccc;
  stroke: #333;
  stroke-width: 1.5px;
}
</style>
</head>
<body>
<div id="canvas"></div>
<script>
  var width = 800, height = 600;
  var MILE=1609, KM=1000, RE=6371*KM, DEGREE=Math.PI/180;
  var query, nodes, links, mapCenter, mapScale, unitDistance=KM;

  mapCenter = [8.0, 47.0], mapScale = 1800;
  query = {"start": "Paris", "end": "Cannes"};
  nodes = [
    {name: "Paris", lon: 2.3508, lat: 48.8567},
    {name: "Lyon", lon: 4.84, lat: 45.76},
    {name: "Marseille", lon: 5.37, lat: 43.2964},
    {name: "Bordeaux", lon: -0.58, lat: 44.84},
    {name: "Cannes", lon: 7.0128, lat: 43.5513},
    {name: "Toulouse", lon: 1.444, lat: 43.6045},
    {name: "Reims", lon: 4.0347, lat: 49.2628}
  ];
  links = [
    {source:"Paris", target:"Lyon", distance:464},
    {source:"Paris", target:"Bordeaux", distance:582},
    {source:"Paris", target:"Reims", distance:144},
    {source:"Lyon", target:"Marseille", distance:314},
    {source:"Marseille", target:"Cannes", distance:175},
    {source:"Marseille", target:"Toulouse", distance:403},
    {source:"Bordeaux", target:"Toulouse", distance:245}
  ];

  links = links.concat(createReverseLinks(links));
  var result = astarRouting(nodes, links);

  document.write(result.route.join(', ')+" ("+String(result.calculationTime)+" ms)<br>");

  markRoute(result.path);
  drawMap(nodes, links);


  function createReverseLinks(links) {
    return links.map(function(d) {return {"source":d.target, "target":d.source, "distance":d.distance}})
  }
  function markRoute(routePath) {
    routePath.forEach(function(i) {links[i.index]["route"]=true;});
  }
  
  function astarRouting(nodes, links) {
  
    function GeoGraph(nodes, links) {
      this.nodes = {};
      this.links = links;
      for (var i = 0; i < nodes.length; ++i) {
        var node = nodes[i],
            obj = new GeoNode(node.name, +node.lon, +node.lat);
        this.nodes[obj.name] = obj;
      }
      this.init();
    }
  
    GeoGraph.prototype.init = function() {
      this.dirtyNodes = [];
      for (var k in this.nodes) {
        astar.cleanNode(this.nodes[k]);
      }
    };
    GeoGraph.prototype.cleanDirty = Graph.prototype.cleanDirty;
    GeoGraph.prototype.markDirty = Graph.prototype.markDirty;
  
    GeoGraph.prototype.neighbors = function(node) {
      var nodes=this.nodes, 
          neighs=createNeighbors(node,this.links);
      return neighs.map(function(name) {return nodes[name];});
    };
  
    createNeighbors = function(node, links) {
    var neighs = node.neighbors;
    if (!Object.keys(neighs).length) {
      for (var n,i=links.length-1; i>=0; i--) {
        if (links[i].source==node.name) {
          n=links[i].target;
          neighs[n] = new Neighbor(i, links[i].distance*unitDistance);
        }
      }
      node.neighbors = neighs;    
      }
    return Object.keys(neighs);
    };
  
    function Neighbor(index, cost) {
      this.index = index;
      this.cost = cost;
    }
  
    function GeoNode(name, lon, lat) {
      this.name = name;
      this.lon = lon*DEGREE;
      this.lat = lat*DEGREE;
      this.neighbors = {};
    }
  
    GeoNode.prototype.weight = 1;
    GeoNode.prototype.isWall = function() {
      return this.weight === 0;
    };
    GeoNode.prototype.getCost = function(nodeSource) {
      return nodeSource.neighbors[this.name].cost;
    };
  
    GeoNode.prototype.getLinks = function(node) {
      return this.neighbors[node.name];
    };
  
    var GeoDistance = function(p, q) {
      var y=p.lat-q.lat, x=(p.lon-q.lon)*Math.cos((p.lat+q.lat)/2)
      return Math.hypot(x,y)*RE;
    };
  
    var graph = new GeoGraph(nodes, links);
    var startEnd = ["start", "end"].map(function(x) {return graph.nodes[query[x]];});
    var sTime = new Date();
    var result = astar.search(graph, startEnd[0], startEnd[1], {heuristic: GeoDistance});
    var deltaTime = new Date() - sTime;
    var route = [query["start"]].concat(result.map(function(x) {return x.name}));
    var path = result.map(function(x) {return x.parent.getLinks(x);});
    return {"route": route, "path": path, "calculationTime": deltaTime};
  }
  
  function drawMap(nodes, links) {
    var mapzoom = d3.mapzoom()
        .center(mapCenter)
        .scale(mapScale);
    var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height)
        .call(mapzoom);
    var frame = svg.append("g");
    
    mapzoom.addTileLayer(frame, "tiles.mapbox.com/v4/mapbox.light/", ".png");
    
    var projection = mapzoom.projection(), nodesByName = {};
    
    nodes.forEach(function(d) {
      d.coord = [+d.lon, +d.lat]
      nodesByName[d.name] = d;
      d.links = [];
    });
    links.forEach(function(d) {
      d.source = nodesByName[d.source]
      d.target = nodesByName[d.target]
      d.distance = +d.distance;
    });
  
    var link = svg.append("g").selectAll(".link")
      .data(links)
      .enter().append("path")
      .attr("class", "link")
      .style("stroke", "#00c000")
      .style("stroke-width", 1);
  
    var node = svg.append("g").selectAll(".node")
        .data(nodes)
        .enter().append("circle")
        .attr("class", "node")
        .attr("r", 3)
        .attr("fill", "grey")
  
    node.append("title")
        .text(function(d) { return d.name; });
  
    midPoint = function(a,b) {
      var n=7;
      return [(a[0]*n+b[0])/(n+1),(a[1]*n+b[1])/(n+1)];
    }

    mapzoom.addLayer(function() {
        node
            .each(function(d) {d.proj = projection(d.coord);})
            .attr("transform", function(d) { return "translate(" + d.proj + ")" })
        link
            .attr("d", function(d) { return "M " + d.source.proj + " L " + midPoint(d.source.proj,d.target.proj) + " L " + d.target.proj })
            .attr("marker-mid", function(d) {if(d.route===true) return 'url(#arrowhead)';});
     });
  
    svg.append('defs').append('marker')
        .attr({'id':'arrowhead',
               'viewBox':'0 -5 10 10',
               'refX':0,
               'refY':0,
               'orient':'auto',
               'markerWidth':10,
               'markerHeight':10,
               'xoverflow':'visible'})
        .append('svg:path')
            .attr('d', 'M 2,0 L 0,-3 L 10,0 L 0,3')
            .attr('fill', '#0080ff');
  }
</script>
</body>
</html>
```