[JAVA] Let's make a Force-Graph of railway network from Tokyo public transportation open data

Tokyo Public Transport Open Data Challenge

In Tokyo Public Transportation Open Data Challenge, major public transportation data in the metropolitan area is released as open data, and the "3rd Tokyo Public Transportation Open Data Challenge" If you enter, an access token will be issued and you can use the API. I got to know the site during GW and tried to enter for the time being.

Overview

Since I live in a rural area, I don't know much about the railway network in the metropolitan area. For this reason, the metropolitan area railway that combines information organization and study of Tokyo Open Data Challenge API and D3.js. I made a network Force-Graph.

Contents / procedures

1. Organize data with ODPT Train API

We collected route and station information with "ODPT Train API" and organized the data with java. The source code is shown below. The http client uses OkHttp, and JSON processing uses Gson. A Railline instance is generated by extracting the "line name", "unique identifier", and "line station" of the line from the response (JSON) of the "ODPT Train API", and the "station name" and "unique identifier" of the station in the line , And a Station instance is generated from "Relevant route identifier". Stations seem to have unique identifiers assigned to each line, so information is aggregated by "station name (Japanese name)".

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class Test {
	private static final String URL_TOKYO_CH="https://api-tokyochallenge.odpt.org/api/v4/";
	private static final String KEY_TOKYO_CH="Access token";

	@SuppressWarnings({ "unused", "rawtypes", "unchecked" })
	public static void main(String[] args){
        OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
        okHttpBuilder.connectTimeout(20, TimeUnit.SECONDS);
        okHttpBuilder.readTimeout(20, TimeUnit.SECONDS);
        okHttpBuilder.writeTimeout(20, TimeUnit.SECONDS);
        OkHttpClient client=okHttpBuilder.build();
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        List<Map> list=trainAPI(client,gson,"odpt:Railway");
        Map<String,Station> stations=new HashMap<String,Station>();
        List<RailLine> raillines=new ArrayList<RailLine>();
    	for(Map map : list){
    		RailLine line=new RailLine();
    		line.name_ja=((Map)map.get("odpt:railwayTitle")).get("ja").toString();
    		line.name_en=((Map)map.get("odpt:railwayTitle")).get("en").toString();
    		line.sameAs=map.get("owl:sameAs").toString();
    		line.operator=map.get("odpt:operator").toString();
    		List<Map> ll=(List<Map>)map.get("odpt:stationOrder");
    		for(Map o : ll){
    			String st=((Map)o.get("odpt:stationTitle")).get("ja").toString();
    			line.stations.add(st);
    			if(stations.containsKey(st)){
    				Station s=stations.get(st);
    				s.lines.add(line.name_ja);
    			}else{
    				Station s=new Station();
    				s.sameAs=o.get("owl:sameAs").toString();
    				s.name_ja=((Map)o.get("odpt:stationTitle")).get("ja").toString();
    				s.name_en=((Map)o.get("odpt:stationTitle")).get("en").toString();
    				s.lines.add(line.sameAs);
    				stations.put(s.name_ja, s);
    			}
    		}
    		raillines.add(line);
    	}
    	Map<String,Object> ret=new HashMap<String,Object>();
    	ret.put("stations", stations);
    	ret.put("raillines", raillines);
        File f=new File("railway.json");
        BufferedWriter bw=null;
        try{
        	bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f),"UTF-8"));
        	bw.write(gson.toJson(ret));
        	bw.flush();
        	bw.close();
        	bw=null;
        }catch(Exception e){
        	e.printStackTrace();
        }finally{
        	if(bw!=null){
        		try{bw.close();}catch(Exception e){}
        	}
        }
	}

	@SuppressWarnings("unchecked")
	private static List<Map> trainAPI(OkHttpClient client,Gson gson,String odpc){
		String url=URL_TOKYO_CH+odpc+"?acl:consumerKey="+KEY_TOKYO_CH;
		System.out.println(url);
        try{
    		Request request = new Request.Builder()
                    .url(url)
                    .get()
                    .build();
            Response response = client.newCall(request).execute();
    		return gson.fromJson(response.body().string(), List.class);
        }catch(Exception e){
        	e.printStackTrace();
        	return null;
        }
	}

	static class Station{
		public String name_ja;
		public String name_en;
		public String sameAs;
		public List<String> lines=new ArrayList<String>();
	}
	static class RailLine{
		public String name_ja;
		public String name_en;
		public String sameAs;
		public String operator;
		public List<String> stations=new ArrayList<String>();
	}
}

2. Output JSON

Executing the above code will output the following JSON file. Looking at this, I feel that my understanding of the railway network in the metropolitan area has increased, saying, "There are such lines and there are stations like this."

{
  "raillines": [
    {
      "name_ja": "Tokyo Sakura Tram (Toden-Arakawa Line)",
      "name_en": "Tokyo Sakura Tram (Arakawa Line)",
      "sameAs": "odpt.Railway:Toei.Arakawa",
      "operator": "odpt.Operator:Toei",
      "stations": [
        "Minowa Bridge",
        "Arakawa-Itchumae",
        "In front of Arakawa Ward Office",
        "Arakawa 2-chome",
        "Arakawa 7-chome",
        "In front of Machiya station",
        "Machiya 2-chome",
        "Higashiogu 3-chome",
        "In front of Kumanomae",
        "Miyano-mae",
        "Odai",
        "In front of Arakawa Amusement Park",
        "In front of Arakawa garage",
        "Kajiwara",
        "Sakaecho",
        "In front of Oji Station",
        "Asukayama",
        "Takinogawa 1-chome",
        "Nishigahara 4-chome",
        "Shinkoshinzuka",
        "Koshinzuka",
        "Sugamoshinden",
        "In front of Otsuka station",
        "Mukaihara",
        "Higashiikebukuro 4-chome",
        "Toden Zoshigaya",
        "Kishimojinmae",
        "Gakushuinshita",
        "Omokage Bridge",
        "Waseda"
      ]
    },
/*****abridgement*******/
  "stations": {
    "Serada": {
      "name_ja": "Serada",
      "name_en": "Serada",
      "sameAs": "Serada",
      "lines": [
        "odpt.Railway:Tobu.Isesaki"
      ]
    },
    "Higashi Tokorozawa": {
      "name_ja": "Higashi Tokorozawa",
      "name_en": "Higashi-Tokorozawa",
      "sameAs": "Higashi Tokorozawa",
      "lines": [
        "odpt.Railway:JR-East.Musashino"
      ]
    },
/*****abridgement*******/

3. Display Force-Graph in D3.js

I loaded the route / station information into D3.js and tried to generate Force-Graph. I made Force-Graph with D3.js for the first time, but I was surprised that it was very easy to make compared to writing GraphLayout with java. D3.js is amazing.

<!DOCTYPE html>
<html>
<head>
    <title>tokyo-challenge-test</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.2/d3.min.js"></script>
</head>
<body>
<svg></svg>
<script type="text/javascript">
    let width = 1200;
    let height = 800;
    const loadData = () => {
        d3.json("railway.json").then(function(json) {
            createMap(json);
        });
    };
    const createMap=(json)=>{
        const rail=json.raillines;
        const station=json.stations;
        let nodes=[];
        let links=[];
        let check={};
        let idv=0;
        for(let i=0;i<rail.length;i++){
            let sts=rail[i].stations;
            let tmp=[];
            for(let j=0;j<sts.length;j++){
                if(!check[sts[j]]){
                    let p={id:idv++,label:station[sts[j]].name_ja,val:1};
                    tmp.push(p);
                    nodes.push(p);
                    check[sts[j]]=p;
                }else{
                    check[sts[j]].val=check[sts[j]].val+1;
                    tmp.push(check[sts[j]]);
                }
            }
            for(let i=1;i<tmp.length;i++){
                let l={source:tmp[i-1].id,target:tmp[i].id};
                links.push(l);
            }
        }
        const svg = d3.select("svg").attr("width",width).attr("height",height);
        const link = d3.select("svg")
            .selectAll("line")
            .data(links)
            .enter()
            .append("line")
            .attr("stroke-width", 1)
            .attr("stroke", "#ccc");
        const node = d3.select("svg")
            .selectAll("g")
            .data(nodes)
            .enter()
            .append("circle")
            .attr("r",function(d){return d.val*5;})
            .attr("fill", "orange")
            .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
        const label = d3.select("svg")
            .selectAll("g")
            .data(nodes)
            .enter()
            .append("text")
            .attr("text-anchor", "middle")
            .attr("dominant-baseline", "middle")
            .style("fill", "steelblue")
            .style("font-size", "9px")
            .text(function(d){return d.label;});
        const simulation = d3.forceSimulation()
            .force("link", d3.forceLink())
            .force("center", d3.forceCenter(600, 450))
            .force("charge", d3.forceManyBody().strength(-8))
            .force("x", d3.forceX().strength(0.05).x(width / 2))
            .force("y", d3.forceY().strength(0.05).y(height / 2));

        simulation.nodes(nodes).on("tick", ticked);
        simulation.force("link").links(links);
        function ticked() {
            link.attr("x1", function(d) { return d.source.x; })
                .attr("y1", function(d) { return d.source.y; })
                .attr("x2", function(d) { return d.target.x; })
                .attr("y2", function(d) { return d.target.y; });
            node.attr("cx", function(d) { return d.x; })
                .attr("cy", function(d) { return d.y; });
            label.attr("x", function(d) { return d.x; })
                .attr("y", function(d) { return d.y; });
        }
        function dragstarted(d) {
            if(!d3.event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }
         function dragged(d) {
            d.fx = d3.event.x;
            d.fy = d3.event.y;
        }
        function dragended(d) {
            if(!d3.event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }
        const zoom = d3.zoom()
            .scaleExtent([1/4,4])
            .on('zoom', function(){
                node.attr("transform", d3.event.transform);
                link.attr("transform", d3.event.transform);
                label.attr("transform", d3.event.transform);
            });
        svg.call(zoom);
    }
    loadData();
</script>
</body>
</html>

Finally

There are too many stations and lines, so the graph is not clear, but it was surprising that there are more lines at Shinjuku Station and Shibuya Station than at Ueno Station. I would like to devise a little more display and add data such as distances between stations and fares to play.

Recommended Posts

Let's make a Force-Graph of railway network from Tokyo public transportation open data
Let's make a pseudo model using active_hash ~ Prefecture data ~
Let's make a robot! "A simple demo of Java AWT Robot"