逆に考えてみた。

略地図って場所や量を制限して描いてるけど、そうじゃなくて描いたあとに削ってみたらいいんじゃないかって。

たとえばこんな感じ。

名古屋の栄駅から巫女コスプレ居酒屋『月天』へのルート。
切り取り地図 (clipped map)

使った経路データは やすらぎ居酒屋 月天 - ルートラボ - LatLongLab のKMLファイル。

なかなか(・∀・)イイ感じじゃない?

比較用にYahoo!の経路地図APIを出力してみる。
経路地図

他の経路もいくつか試してみる。

データ: [ヅ] RunKeeperのGPXとKMLをダウンロードする

切り取り地図:
切り取り地図 (clipped map)

経路地図:
経路地図

データ: [Nagoya Castle's front gate] -> [a front of Nagoya Station] -> [Electrical Museum] -> [SANGETSU] -> [Meijo Park] - ルートラボ - LatLongLab

切り取り地図:
切り取り地図 (clipped map)

経路地図:
経路地図

データ: 三つの橋 {the three bridges} - ルートラボ - LatLongLab

切り取り地図:
切り取り地図 (clipped map)

経路地図:
経路地図

データ: 名古屋栄駅 〜 Vie Sa Vie (ヴィ・サ・ヴィ) - ルートラボ - LatLongLab

切り取り地図:
切り取り地図 (clipped map)

経路地図:
経路地図

データ: ALPSLAB route (名古屋駅〜フェリーチェ・ぎゃろりいの(Felice Giallolino)) のデータ

切り取り地図:
切り取り地図 (clipped map)

経路地図:
経路地図

ALPSLAB 略地図(データとスクリーンショット取っといて良かった):
ALPSLAB 略地図

文字が途中で消えちゃうこともあったり、経路データが長すぎると地図画像が生成できないとか、いろいろあるけどまぁまぁいいかも。

以下、ソースコード。切り取り地図の生成まで書くのに1時間ぐらい。そこからKML処理とかテストデータ探すとかいろいろやってたら計3時間ぐらいかかった。。。


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.imageio.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;
 
public class ClippedMap {
 
  public static void main(String[] args) throws Exception {
  
    // ALPSLAB route (名古屋駅〜フェリーチェ・ぎゃろりいの(Felice Giallolino)) のデータ
    // http://www.nilab.info/lab/alpslab_route/felice_giallolino/
    writeClippedMap("http://www.nilab.info/lab/alpslab_route/felice_giallolino/17ba55406169fdc50eac99ef474deeb7.kml", 400, 400);
    
    // [ヅ] RunKeeperのGPXとKMLをダウンロードする
    // http://www.nilab.info/z3/20110830_04.html
    writeClippedMap("http://www.nilab.info/f/0/RK_kml_2010-09-28_0556PM.kml", 600, 600);
      
    // 名古屋栄駅 〜 Vie Sa Vie (ヴィ・サ・ヴィ) - ルートラボ - LatLongLab
    // http://latlonglab.yahoo.co.jp/route/watch?id=5f54f25756fc0242734dd5d8865ca2d4
    writeClippedMap("http://latlonglab.yahoo.co.jp/route/get?id=5f54f25756fc0242734dd5d8865ca2d4&format=kml", 500, 500);
    
    // 三つの橋 {the three bridges} - ルートラボ - LatLongLab
    // http://latlonglab.yahoo.co.jp/route/watch?id=acb7164917ea4b80a5b125924f1ced2e
    writeClippedMap("http://latlonglab.yahoo.co.jp/route/get?id=acb7164917ea4b80a5b125924f1ced2e&format=kml", 600, 600);
    
    // やすらぎ居酒屋 月天 - ルートラボ - LatLongLab
    // http://latlonglab.yahoo.co.jp/route/watch?id=43b640cbbb224e3d061da6a68f0e897c
    writeClippedMap("http://latlonglab.yahoo.co.jp/route/get?id=43b640cbbb224e3d061da6a68f0e897c&format=kml", 600, 600);
      
    // [Nagoya Castle's front gate] -> [a front of Nagoya Station] -> [Electrical Museum] -> [SANGETSU] -> [Meijo Park] - ルートラボ - LatLongLab
    // http://latlonglab.yahoo.co.jp/route/watch?id=7609e509a4b0b0b420f78b5046f7f222
    writeClippedMap("http://latlonglab.yahoo.co.jp/route/get?id=7609e509a4b0b0b420f78b5046f7f222&format=kml", 600, 600);
  }
 
  private static void writeClippedMap(String kmlUrl, int imageWidth, int imageHeight) throws Exception {
    Route route = getRoute(kmlUrl);
    // 地図の上下左右の緯度経度を取得
    MapParams params = new MapParams(route, imageWidth, imageHeight);
    String mapInfoUrl = getStaticMapUrl(params, "xml");
    MapInfo mapinfo = getMapInfo(mapInfoUrl);
    // 経路の緯度経度列をピクセル座標へ変換
    GeoTranslator gt = new GeoTranslator(
      mapinfo.minlat, mapinfo.maxlat,
      mapinfo.minlon, mapinfo.maxlon,
      0, imageWidth, 0, imageHeight);
    Path2D pxroute = new Path2D.Double();
    Point2D pos = gt.toPoint(route.lat(0), route.lon(0));
    pxroute.moveTo(pos.getX(), pos.getY());
    for(int i=0; i<route.size(); i++){
      pos = gt.toPoint(route.lat(i), route.lon(i));
      pxroute.lineTo(pos.getX(), pos.getY());
    }
    // 画像オブジェクトを生成
    BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    // クリッピング領域を設定
    float clippingWidth = 160.0f;
    BasicStroke pen = new BasicStroke(clippingWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
    Shape clippingShape = pen.createStrokedShape(pxroute);
    g.setClip(clippingShape);
    // 地図画像を取得して描画(経路は透明色に設定している、つまり描画しない)
    String mapImageUrl = getStaticMapUrl(params, "png32");
    BufferedImage mapImage = ImageIO.read(new URL(mapImageUrl));
    g.drawImage(mapImage, 0, 0, null);
    // 自前で経路を描画する(たとえば好きな画像のパターンを貼り付けることも可能になる)
    g.setPaint(new Color(0, 0, 255, 127));
    g.fill(new BasicStroke(10.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND).createStrokedShape(pxroute));
    g.dispose();
    String id = "" + System.currentTimeMillis();
    ImageIO.write(image, "png", new File("20120216_clippedmap_" + id + ".png"));
    // 比較用に経路地図APIを出力する
    String routeMapImageUrl = getRouteMapUrl(params, "png32");
    BufferedImage routeMapImage = ImageIO.read(new URL(routeMapImageUrl));
    ImageIO.write(routeMapImage, "png", new File("20120216_routemap_" + id + ".png"));
  }
  
  // KMLから緯度経度を抜き出してRouteオブジェクトを生成
  private static Route getRoute(String kmlUrl) throws Exception {
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(kmlUrl);
    XPath xpath = XPathFactory.newInstance().newXPath();
    String coordinates = xpath.evaluate("//coordinates", doc);
    String[] a = coordinates.split("\\s");
    Route route = new Route();
    for(int i=0; i<a.length; i++){
      String[] lonlat = a[i].split(","); // lon.lat,alt
      if(lonlat.length >= 2){
        route.add(new Double(lonlat[1]), new Double(lonlat[0]));
      }
    }
    return route;
  }
  
  // 経路クラス
  private static class Route{
    // [緯度,経度,緯度,経度,緯度,経度,...]
    ArrayList<Double> geopos = new ArrayList<Double>();
    private void add(double lat, double lon){
      geopos.add(lat);
      geopos.add(lon);
    }
    private int size(){
      return geopos.size() / 2;
    }
    private double lat(int index){
      return geopos.get(index * 2);
    }
    private double lon(int index){
      return geopos.get(index * 2 + 1);
    }
    private String getString(){
      StringBuilder sb = new StringBuilder();
      for(int i=0; i<geopos.size(); i++){
        sb.append(geopos.get(i));
        sb.append(",");
      }
      sb.deleteCharAt(sb.length() - 1); // 最後のカンマを削除
      return sb.toString();
    }
  }
  
  // 経路地図画像のURLを返す
  private static String getRouteMapUrl(MapParams p, String output){
    // Yahoo!デベロッパーネットワーク - YOLP(地図) - 経路地図
    // http://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/routemap.html
    String url = 
      "http://routemap.olp.yahooapis.jp/OpenLocalPlatform/V1/routeMap" +
      "?appid=" + p.appid +
      "&output=" + output +
      "&width=" + p.width +
      "&height=" + p.height +
      "&route=search:off|" + p.route.getString();
    return url;
  }
  
  // 地図画像のURLを返す
  private static String getStaticMapUrl(MapParams p, String output){
    // Yahoo!デベロッパーネットワーク - YOLP(地図) - スタティックマップ
    // http://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/static.html
    String url = 
      "http://map.olp.yahooapis.jp/OpenLocalPlatform/V1/static" +
      "?appid=" + p.appid +
      "&output=" + output +
      "&width=" + p.width +
      "&height=" + p.height +
      "&l=" + p.r + "," + p.g + "," + p.b + "," + p.a + "," + p.w + "," + 
      p.route.getString();
    return url;
  }
  
  // 地図の情報を返す
  private static MapInfo getMapInfo(String url) throws Exception {
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(url);
    XPath xpath = XPathFactory.newInstance().newXPath();
    // datum="WGS84" format="lon,lat"
    String[] ul = xpath.evaluate("ResultSet/Result/Coordinate-UL/Coordinates", doc).split(","); // 左上
    String[] ur = xpath.evaluate("ResultSet/Result/Coordinate-UR/Coordinates", doc).split(","); // 右上
    String[] dl = xpath.evaluate("ResultSet/Result/Coordinate-DL/Coordinates", doc).split(","); // 左下
    String[] dr = xpath.evaluate("ResultSet/Result/Coordinate-DR/Coordinates", doc).split(","); // 右下
    // 上下左右の緯度経度の平均を求める
    MapInfo mi = new MapInfo();
    mi.maxlat = (Double.parseDouble(ul[1]) + Double.parseDouble(ur[1])) / 2; // 上端
    mi.minlat = (Double.parseDouble(dl[1]) + Double.parseDouble(dr[1])) / 2; // 下端
    mi.minlon = (Double.parseDouble(ul[0]) + Double.parseDouble(dl[0])) / 2; // 左端
    mi.maxlon = (Double.parseDouble(ur[0]) + Double.parseDouble(dr[0])) / 2; // 右端
    return mi;
  }
  
  // 地図の情報クラス
  private static class MapInfo{
    double minlat;
    double maxlat;
    double minlon;
    double maxlon;
  }
  
  // 地図生成パラメータクラス
  private static class MapParams{
    Route route;
    String appid = "YOUR APPLICATION ID";
    int r = 0;
    int g = 0;
    int b = 0;
    int a = 127;
    int w = 10;
    int width;
    int height;
    MapParams(Route route, int width, int height){
      this.route = route;
      this.width = width;
      this.height = height;
    }
  }
  
  // 緯度経度をピクセル座標へ変換するクラス(単純な割合で計算。投影法とかわからんし・・・)
  private static class GeoTranslator{
    private final double fx;
    private final double fy;
    private final double minx;
    private final double miny;
    private final double minlon;
    private final double maxlat;
    // 緯度経度は右上方向が正
    // ピクセル右下方向が正
    private GeoTranslator(
      double minlat, double maxlat, double minlon, double maxlon,
      double minx, double maxx, double miny, double maxy){
      this.fx = (maxx - minx) / (maxlon - minlon);
      this.fy = (maxy - miny) / (maxlat - minlat);
      this.minx = minx;
      this.miny = miny;
      this.minlon = minlon;
      this.maxlat = maxlat;
    }
    // 緯度経度をピクセル座標へ変換する
    private Point2D toPoint(double lat, double lon){
      double x = (lon - minlon) * fx + minx;
      double y = (maxlat - lat) * fy + miny;
      return new Point2D.Double(x, y);
    }
  }

}

やっぱり、Javaの描画ライブラリは便利。 BasicStroke が無かったらこんなのサクっと書けない。。。

「制限して描く」んじゃなくて「消す」とか「削る」っていう逆転の発想は他にも使えないかな・・・

Ref.
- Yahoo!デベロッパーネットワーク - YOLP(地図) - スタティックマップ
- Yahoo!デベロッパーネットワーク - YOLP(地図) - 経路地図

tags: map yahoo_maps_api routemap java

Posted by NI-Lab. (@nilab)