前回までで地図描画用の都道府県輪郭データを取得出来たので、ようやく白地図の出力処理に取り掛かれます。
 
 地図出力の方法としてはHTML5のCanvasなどもありますが、今回はお手軽にGoogleMapsAPIを利用することにしました。
 下図は前回作成したものですが、日本列島については都道府県別にポリゴン表示出来ているものの、中国や朝鮮半島、あるいは海などの背景も表示されているので「白地図」としては適していません。まずは、これらの情報を消す処理から始めます。

図1

図1 都道府県の輪郭をGoogleMapsAPIにポリゴン出力
前回作成したもの。背景があるので「白地図」とはいえません

 GoogleMapsAPIの地図のスタイルは、MapOptionsオブジェクトの.stylesプロパティにて指定することが出来ます。

     var styleAllOFF = [
       {
          featureType: 'all',
          stylers: [
             {visibility: 'off'},
          ]
       }
     ];

 上記の設定では全ての背景を消しています。そして、

 
     var myOptions = {
        styles: styleAllOFF
     };
     map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

 といった感じでMAP生成時にオプション指定するだけです(図2)。後から、map.setOptionsメソッドで設定することも出来ます。

図2

図2 .stylesプロパティにて全てのオブジェクト表示をOFF
背景は消えましたがグレーです


 GoogleMapsAPIでは地の色はグレーのようです。これでも悪くはないのですが、「白地図」作成を目指す以上はやはり背景は白でないと・・・。これは上述のMAPオプションに、
   backgroundColor: ‘#ffffff’
を追加するだけで実現出来ます(図3)。
 ※後からsetOptionsで変更することも試みましたが出来ませんでした。記述が間違っていたのか、そういう仕様なのか?
図3

図3 MAP生成時のオプションでbackgroundColorを設定
白地図っぽくなりました

 ただ、GoogleMapsAPIのバグなのか、ズーム変更時に何故か元の背景が残ってしまう(韓国の辺りだけなんですが)ことがあります(図4)。

図4

図4 上の図3のズーム時の不具合?
韓国周辺のブロックのみ背景が残ってしまっています


 まあ、一時的なバグですぐにFixされるかもしれませんし、大した問題でもないのですが、学習も兼ねて対応方法を考えることにしました。

 そこで思いついたのが、巨大な白い図形で地図全体を覆ってしまう方法。
 GoogleMapsAPIには、簡単に矩形を描画するためのクラス「Rectangle」が用意されているので、これを利用します。

 
  function drawRectangle() {

     var rectangle = new google.maps.Rectangle();
     var rectBounds = new google.maps.LatLngBounds(
              new google.maps.LatLng(-90.0, -180),
              new google.maps.LatLng(90.0, 180));
     var rectOptions = {
        strokeColor: "#FFFFFF",
        strokeOpacity: 1.0,
        strokeWeight: 0.0,
        fillColor: "#FFFFFF",
        fillOpacity: 1.0,
        zIndex: 0,
        map: map,
        bounds: rectBounds
     };
     rectangle.setOptions(rectOptions);

  }

 これで、全体を白くすることが出来ました。後で描画するものがこの矩形の下に隠れてしまう場合もあるので、zIndex=0として一番下層に配置させています(13行目)。
 ズーム変更時に一瞬だけ背景が表示されますが、これは前述した.stylesプロパティの変更と併用することで避けられます。
 
 
 ・・・と、ここまででは単なる「白い地図」なので、これを操作可能なものに加工します。今回は次のような機能を持たせたいと思います。

 1.ポリゴンにクリック時イベント等のアクションを付加する。アクションは領域単位ではなく、都道府県単位となるようにする。例えば、佐渡島にマウス移動させた時には新潟県全体が選択されるようにする。
 2.ポリゴンのマウスオーバー時に都道府県の輪郭線を太くする。マウスアウト時に元に戻す。
 3.都道府県選択ののセレクトボックス、カラーピッカー(カラー選択ボックス)及びペイントボタンを設置。ペイントボタンをクリックすると、選択した都道府県領域が選択した色でペイントされる。
 4.都道府県ポリゴンのクリック時イベントに上記ペイント機能を付加。右クリック時にはペイントが取り消される(白でペイント)仕様とする。
 
 
 カラーピッカーは、HTML5で追加されたINPUT要素「type=”color”」で実装出来るのですが、まだ対応ブラウザが少なく現時点ではIEでは利用出来ません。そのためjQueryプラグインを利用することに・・・何種類か試した中で、最も使い易いと感じた「Spectrum」を採用しました。

 Spectrum – The No Hassle jQuery Colorpicker

 実装方法は簡単です。上記サイトでダウンロードしたファイルに含まれるspectrum.jsとspectrum.cssを設置し、HTMLヘッダ部に

<link href="css/spectrum.css" rel="stylesheet" type="text/css"> <!-- spectrum.cssの保存先 -->
<script type="text/javascript" src="js/jquery.js"></script>     <!-- jQueryの保存先 -->
<script type="text/javascript" src="js/spectrum.js"></script>   <!-- spectrum.jsの保存先 -->

 といった具合に記載します。
 次にカラーピッカーを表示させたい箇所にINPUT要素を設置

   <input type='text' class="basic"/>

 Javascriptもシンプルで、ベーシックな設定であればこれだけ

   $(function(){
      $(".basic").spectrum({ //ここのクラス名をinput要素のクラスと合わせる。#をつけてidで結びつけてもOK
         color: "#f00",
      });
   })

 サンプルはこちら。※上記のシンプルなものにカラー変更時イベントを追加しています。
 この他、いろいろとカスタマイズ可能です。詳細は上記のリンクサイトでご確認下さい。
 
 

 続いて、今回の白地図表示の箱となるHTMLを用意

・blankmap.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>都道府県の輪郭表示 ~Web上で操作可能な日本の白地図(都道府県別)を作る~</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="Content-Style-Type" content="text/css">
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="http://code.google.com/apis/gears/gears_init.js"></script>
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/spectrum.js"></script>
<link rel='stylesheet' href='css/spectrum.css'>
<script type="text/javascript" src="js/blankmap.js"></script>

</head>

<body style="margin:0px;" onload="initialize()">
  <div id="top_bar" style="background-color:#333399;padding:5px;">
  <table>
  <tr>
    <th class="map_title" id="map_title" name="map_title" style="font-size: 16px;color: #FFFFFF;">都道府県の輪郭を指定した色でペイント ~Web上で操作可能な日本の白地図(都道府県別)を作る~</td>
  </tr>
  </table>
  <form id="f_paintmenu" name="f_paintmenu">
  <table>
  <tr>
    <td style="font-size:12px;color:#FFFFFF;padding-left:10px;">都道府県:
        <select id="f_pref" name="f_pref" style="width:100px;font-size:12px;">
        </select>
    </td>
    <td style="font-size:12px;color:#FFFFFF;padding-left:10px;">色の指定:
        <input type='text' id="f_colorpicker" value="#FF0000">
    </td>
    <td style="padding-left:10px;">
       <input type="button" style="width:60px;font-size:14px;" id="btnPaintPref" name="btnPaintPref" onclick="paintPref(document.getElementById('f_pref').value,document.getElementById('f_colorpicker').value)" value="ペイント">
    </td>
  </tr>
  </table>
  </form>
  </div>
  <div id="map_canvas" style="margin:5px;"></div>
</body>
</html>

 テーブル(MySQL)のデータ取得→JSON出力は以下(PHP)

・getdatajson.php

<?php

   ini_set( 'memory_limit', '1024M' );

   $msg="";
   $jsondata="";
   $mysqli = connectDBi($msg);

   if ($mysqli) {
      if (isset($_REQUEST["mode"])) {
         if ($_REQUEST["mode"] == "border") {
            if (getPrefBorderJSON($mysqli,$jsondata)) {
               header("Content-Type: text/javascript; charset=utf-8");
               print $jsondata;
            } else {
               print "err:都道府県輪郭線データの取得に失敗しました。";
            }
         } else if ($_REQUEST["mode"] == "preflist") {
            if (getPrefListJSON($mysqli,$jsondata)) {
               header("Content-Type: text/javascript; charset=utf-8");
               print $jsondata;
            } else {
               print "err:都道府県リストの取得に失敗しました。";
            }
         } else {
            print "err:モード設定エラー";
         }
      } else {
         print "err:モードが設定されていません";
      }
      $mysqli->close();
   } else {
      print $msg;
   }
   ini_restore('memory_limit');

   exit();

   function getPrefBorderJSON($mysqli,&$jsondata){

      $p = array();
      $strSQL = "SELECT *";
      $strSQL .= " FROM t_JapanPrefectureBorder";
      $strSQL .= " ORDER BY PrefectureCode, BorderCode, PointNo";
      $former= array();

      $rst = $mysqli->query($strSQL);
      $rcount = $rst->num_rows;
      if ($rcount > 0) {
         while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
            if (!isset($former["PrefectureCode"]) || $col["PrefectureCode"] <> $former["PrefectureCode"]
                  || $col["BorderCode"] <> $former["BorderCode"]) {
               if (!isset($cNo)) {
                  $cNo = 0;
               } else {
                  $cNo++;
               }
               $p[$cNo] = array();
            }
            array_push($p[$cNo],$col);
            $former = $col;
         }
      }
      is_null($rst) or $rst->free();

      $jsondata = json_encode($p); 

      return true;
   }

   function getPrefListJSON($mysqli,&$jsondata){

      $strSQL = "SELECT PrefectureCode, PrefectureName FROM t_Prefecture";
      $strSQL .= " ORDER BY PrefectureCode";

      $rst = $mysqli->query($strSQL);
      $rcount = $rst->num_rows;
      $pref=array();
      if ($rcount > 0) {
         while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
            array_push($pref,$col);
         }
      }
      $jsondata = json_encode($pref); 

      is_null($rst) or $rst->free();

      return true;
   }

   //MySQLへ接続
   function connectDBi(&$err) {

      $MySQL_SERVER = "サーバー名";
      $MySQL_USER = "ユーザー名";
      $MySQL_PASSWORD = "パスワード";
      $MySQL_DBNAME = "データベース名";
      $err = "";
      $mysqli = new mysqli($MySQL_SERVER, $MySQL_USER, $MySQL_PASSWORD, $MySQL_DBNAME);
      if ($mysqli->connect_errno) {
         $err = "err:データベース接続に失敗しました。";
      } else {
         //文字化け対策
         if (!$mysqli->set_charset("utf8")) {
            $err = "err:文字コードセットに失敗しました。";
         }
      }
      if (strlen($err) > 0) {
         return false;
      } else {
         return $mysqli;
      }
   }

?>

 GETパラメータ「mode」の値で、取得するデータの種類を切り替えています。
 mode=border : t_JapanPrefectureBorderより都道府県輪郭情報を取得
 mode=preflist : t_Prefectureより47都道府県リストを取得

 そして、上記PHPの呼び出しを含むjavascriptは以下の通りです。

・blankmap.js

// *************************************************************** 
//  ~Web上で操作可能な日本の白地図(都道府県別)を作る~ 
//
//  テスト:都道府県ポリゴンを指定した色でペイント
//
// *************************************************************** 

  var map;
  var myPolyPref = new Array();
  var OkinawaLine;

  function initialize() {

     setDivSize();
     getPrefList();
     setColorPicker();

     var iniCenter = new google.maps.LatLng(37,138);
     var iniZoom = 6;

     var styleAllOFF = [
        {
           featureType: 'all',
           stylers: [
              {visibility: 'off'},
           ],
        }
     ];

     var myOptions = {
        zoom: iniZoom,
        center: iniCenter,
        backgroundColor: '#ffffff',
        styles: styleAllOFF
     };
     map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

     drawRectangle();
     getPrefBorder();

  }

  //ウィンドウサイズに合わせて地図サイズを調整
  function setDivSize() {

     var w = 1024; //デフォルト値
     var h = 768;  //デフォルト値
     if (window.innerWidth) {
        w = window.innerWidth;
        h = window.innerHeight;
     } else if (document.all) {
        if (document.documentElement.clientWidth) {
           w = document.documentElement.clientWidth;
           h = document.documentElement.clientHeight;
        } else if (document.body.clientWidth) {
           w = document.body.clientWidth;
           h = document.body.clientHeight;
        }
     } else {
        return false;
     }

     var divTop = document.getElementById("top_bar").style;
     var divMain = document.getElementById("map_canvas").style;
     var divHeightTop = 60;

     divTop.height = divHeightTop + "px";
     divMain.width = (w - 20) + "px";
     divMain.height = (h - divHeightTop - 20) + "px";

  }

  //都道府県リストの取得
  function getPrefList() { 

     var fldPref = document.getElementById("f_pref");
     var txtOption = "<option value='0' SELECTED >--------</option>";

     var json;
     jQuery.ajax({
        url : "getdatajson.php?",
        type : "get",
        async : false,
        data: "mode=preflist",
        success: function(request){
                var res = request;
                if (res.substring(0,4) == "err:"){
                   alert("エラー:"+res.substring(4));
                }else{
                   json = JSON.parse(res);
                   for (var i = 0; i < json.length; i++) {
                      txtOption += "<option value='"+json[i].PrefectureCode+"'"
                      if (fldPref.value == json[i].PrefectureCode) {
                         txtOption += " SELECTED";
                      }
                      txtOption += " >"+json[i].PrefectureName+"</option>"
                   }
                }
        },
        error: function() {
                alert('都道府県リストの取得に失敗しました');
        }
     });
     fldPref.innerHTML = txtOption;
  } 

  //都道府県輪郭データの取得
  function getPrefBorder() { 

     var json;
     jQuery.ajax({
        url : "getdatajson.php?",
        type : "get",
        async : false,
        data: "mode=border",
        success: function(request){
                var res = request;
                if (res.substring(0,4) == "err:"){
                   alert("エラー:"+res.substring(4));
                }else{
                   json = JSON.parse(res);
                   setPolygon(json); 
                }
        },
        error: function() {
                alert('都道府県輪郭データの取得に失敗しました');
        }
     });
  } 

  //ポリゴン描画
  function setPolygon(myBorderPoints) {

     var polyCoordsArray = new google.maps.MVCArray();

     myPolyPref.length = 0;
     var polyCoordsPref = new Array();

     for (var i = 1; i <= 47; i++) {
        polyCoordsPref[i] = new google.maps.MVCArray();
     }

     for (var p = 0; p < myBorderPoints.length; p++) {
        var prefCode = parseInt(myBorderPoints[p][0].PrefectureCode);
        var polyCoords = new google.maps.MVCArray();
        
        if (prefCode == 47) {
           moveLocation(myBorderPoints[p],5,15); 
        }
        for (var i = 0; i < myBorderPoints[p].length; i++) {
           polyCoords.push(new google.maps.LatLng(myBorderPoints[p][i].Lat, myBorderPoints[p][i].Lng));
        }
        polyCoordsArray.push(polyCoords);
        polyCoordsPref[prefCode].push(polyCoords);
     }

     for (var i = 1; i <= 47; i++) {
        myPolyPref[i] = new google.maps.Polygon({
           paths: polyCoordsPref[i],
           strokeColor: "#666666",
           strokeOpacity: 1.0,
           strokeWeight: 1,
           fillColor: "#FFFFFF",
           zIndex: 1,
           fillOpacity: 1.0
        });
        myPolyPref[i].setMap(map);
        myPolyPref[i].set("PrefectureCode", i);

        google.maps.event.addListener(myPolyPref[i], "click", function(event) {
           var prefFld = document.getElementById("f_pref");
           for(var j = 0; j < prefFld.length; j++) {
              if (parseInt(prefFld.options[j].value) == this.PrefectureCode) {
                 prefFld.options[j].selected = true;
              } else {
                 prefFld.options[j].selected = false;
              }
           }
           var fColor = document.getElementById("f_colorpicker").value;
           paintPref(this.PrefectureCode,fColor);
        });

        google.maps.event.addListener(myPolyPref[i], "rightclick", function(event) {
           var fColor = "#ffffff";
           paintPref(this.PrefectureCode,fColor);
        });

        google.maps.event.addListener(myPolyPref[i], "mouseover", function(event) {
           this.setOptions({strokeWeight: 2.0});
        });

        google.maps.event.addListener(myPolyPref[i], "mouseout", function(event) {
           this.setOptions({strokeWeight: 1.0});
        });
     }
  }

  //沖縄県の場所を移動(スペースの都合。白地図によくあるケース)
  function moveLocation(myPoints,my,mx) {

     for (var i = 0; i < myPoints.length; i++) {
         myPoints[i].Lat = parseFloat(myPoints[i].Lat) + my;
         myPoints[i].Lng = parseFloat(myPoints[i].Lng) + mx;
     }

     //実際の位置から移動していることを示すライン
     if (!OkinawaLine && parseInt(myPoints[0].PrefectureCode)==47) {
        var OkinawaLineCoords = [
           new google.maps.LatLng(25.7+my, 126.3+mx),
           new google.maps.LatLng(26.7+my, 126.3+mx),
           new google.maps.LatLng(27.7+my, 127.8+mx),
           new google.maps.LatLng(27.7+my, 129.3+mx)
        ];
        OkinawaLine = new google.maps.Polyline({
           path: OkinawaLineCoords,
           strokeColor: "#808080",
           strokeOpacity: 1.0,
           strokeWeight: 1.5,
           zIndex: 1
        });
        OkinawaLine.setMap(map);
     }
  }

  //都道府県の領域(ポリゴン)を指定した色でペイント
  function paintPref(prefcode,fcolor) {
     if (parseInt(prefcode) > 0) {
        myPolyPref[parseInt(prefcode)].setOptions({fillColor: fcolor});
     }
  }

  //巨大な矩形で地図全体を覆う(背景の設定)
  function drawRectangle() {

     var rectangle = new google.maps.Rectangle();
     var rectBounds = new google.maps.LatLngBounds(
              new google.maps.LatLng(-90.0, -180),
              new google.maps.LatLng(90.0, 180));
     var rectOptions = {
        strokeColor: "#FFFFFF",
        strokeOpacity: 1.0,
        strokeWeight: 0.0,
        fillColor: "#FFFFFF",
        fillOpacity: 1.0,
        zIndex: 0,
        map: map,
        bounds: rectBounds
     };
     rectangle.setOptions(rectOptions);

  }

  //カラーピッカー(Spectrum)の設定
  function setColorPicker() {

     var fld = document.getElementById("f_colorpicker");

     $(function(){
        $("#f_colorpicker").spectrum({
          color: fld.value,
          showInput: true,
          showInitial: true,
          showPalette: true,
          showSelectionPalette: true,
          preferredFormat: "hex",
          chooseText: "OK",
          cancelText: "Cancel",
          palette: [
             ["#000000", "#434343", "#666666", "#999999", "#b7b7b7", "#cccccc", "#d9d9d9", "#efefef", "#f3f3f3", "#ffffff"],
             ["#980000", "#ff0000", "#ff9900", "#ffff00", "#00ff00", "#00ffff", "#4a86e8", "#0000ff", "#9900ff", "#ff00ff"],
             ["#e6b8af", "#f4cccc", "#fce5cd", "#fff2cc", "#d9ead3", "#d9ead3", "#c9daf8", "#cfe2f3", "#d9d2e9", "#ead1dc"],
             ["#dd7e6b", "#ea9999", "#f9cb9c", "#ffe599", "#b6d7a8", "#a2c4c9", "#a4c2f4", "#9fc5e8", "#b4a7d6", "#d5a6bd"],
             ["#cc4125", "#e06666", "#f6b26b", "#ffd966", "#93c47d", "#76a5af", "#6d9eeb", "#6fa8dc", "#8e7cc3", "#c27ba0"],
             ["#a61c00", "#cc0000", "#e69138", "#f1c232", "#6aa84f", "#45818e", "#3c78d8", "#3d85c6", "#674ea7", "#a64d79"],
             ["#85200c", "#990000", "#b45f06", "#bf9000", "#38761d", "#134f5c", "#1155cc", "#0b5394", "#351c75", "#741b47"],
             ["#5b0f00", "#660000", "#783f04", "#7f6000", "#274e13", "#0c343d", "#1c4587", "#073763", "#20124d", "#4c1130"]
          ]

        });
     })
  }

・setDivSize(43~71行目)
 ウィンドウサイズに合わせてマップ表示用領域 <div id=”map_canvas”> のサイズを調整します。

・getPrefList(73~105行目)
 jQuery.ajaxでPHPを呼び出して都道府県リストを取得し、それをセレクトボックスにセットします。

・getPrefBorder(107~129行目)
 jQuery.ajaxでPHPを呼び出して都道府県の輪郭データを取得します。取得データはJSON形式の配列で、これを関数setPolygonに引き渡します。

・setPolygon(131~196行目)
 ポリゴン描画を行う、当スクリプトの心臓部です。都道府県毎に制御するため、ポリゴンは配列(myPolyPref)で管理します。
 まずは、

     for (var i = 1; i <= 47; i++) {
        polyCoordsPref[i] = new google.maps.MVCArray();
     }

 と初期化。都道府県数=47なので、本来は0~46に収納するのでしょうが、配列番号は都道府県コードに合わせた方が分かりやすいので1~47としました。

     for (var p = 0; p < myBorderPoints.length; p++) {
        var prefCode = parseInt(myBorderPoints[p][0].PrefectureCode);
        var polyCoords = new google.maps.MVCArray();
         
        if (prefCode == 47) {
           moveLocation(myBorderPoints[p],5,15);
        }
        for (var i = 0; i < myBorderPoints[p].length; i++) {
           polyCoords.push(new google.maps.LatLng(myBorderPoints[p][i].Lat, myBorderPoints[p][i].Lng));
        }
        polyCoordsArray.push(polyCoords);
        polyCoordsPref[prefCode].push(polyCoords);
     }

 この箇所では、引き渡されたJSONデータ(myBorderPoints)の整理をしてpolyCoordsArrayに収納しています。
 元データの配列myBorderPoints[p][i]の要素数は
   p:領域数
   i:領域内の点数
 となっているため、myBorderPoints[p]のままでポリゴン登録すると領域毎となってしまうので、例えば佐渡島は新潟県本土と別のポリゴンとなるので連動しません。そのため、各領域の先頭レコード(myBorderPoints[p][0])の都道府県コードを取得し(144行目)、配列polyCoordsPref[prefCode]の各要素にそれぞれの都道府県の緯度・経度情報を再収納しています(154行目)。
 139~141行目は沖縄県用の処理です(後述)。
 そして、各都道府県についてポリゴン生成しています(157~195行目)。
 ポリゴンオブジェクトには独自のプロパティを持たせることも出来るので、

        myPolyPref[i].set("PrefectureCode", i);

 と「PrefectureCode」プロパティを用意し、都道府県コードを収納しています。
 この値は、次のクリック時イベント

        google.maps.event.addListener(myPolyPref[i], "click", function(event) {
           var prefFld = document.getElementById("f_pref");
           for(var j = 0; j < prefFld.length; j++) {
              if (parseInt(prefFld.options[j].value) == this.PrefectureCode) {
                 prefFld.options[j].selected = true;
              } else {
                 prefFld.options[j].selected = false;
              }
           }
           var fColor = document.getElementById("f_colorpicker").value;
           paintPref(this.PrefectureCode,fColor);
        }); 

 にて、都道府県セレクトボックスの選択値をクリックしたポリゴンの都道府県に切り替える機能(173行目)及び、都道府県をペイントする関数paintPrefの呼び出し(180行目)に利用しています。

        google.maps.event.addListener(myPolyPref[i], "rightclick", function(event) {
           var fColor = "#ffffff";
           paintPref(this.PrefectureCode,fColor);
        });

 右クリック時イベントでもクリック時同様にpaintPrefを呼び出していますが、ペイントカラーを白(#ffffff)にしているので消しゴム的な動作となります。

 以下は、マウスオーバー時、マウスアウト時のポリゴン輪郭(太さ)の変更処理。シンプルです。 

        google.maps.event.addListener(myPolyPref[i], "mouseover", function(event) {
           this.setOptions({strokeWeight: 2.0});
        });
 
        google.maps.event.addListener(myPolyPref[i], "mouseout", function(event) {
           this.setOptions({strokeWeight: 1.0});
        });

・moveLocation(198~223行目)
 本記事の冒頭の図等を見ていただければよく分かりますが、沖縄は本州や九州とかなり離れているので、統計地図では右下部分もしくは左上部分に移動させることが良くあります。本関数ではその移動処理を行っています。
 この他、統計地図では北海道を左上に移動する場合もあり、その際にも利用出来ますが、今回は適用していません。

・paintPref(225~230行目)
 指定した都道府県の輪郭を指定した色でペイントする関数

・drawRectangle(232~251行目)
 前述の「巨大な白い図形で地図全体を覆う」処理

・setColorPicker(253~281行目)
 カラーピッカー(Spectrum)の設定を行っています。

これで、下図のようなことが出来るようになりました。

図5

図5 今回作成の白地図 着色例
このMAPを表示

次回は、輪郭データをテキスト化して、MySQLのない環境でも使えるようにしたいと思います。

コメント   

 2014年7月12日(土)
 私の場合、ジョギングの目的地を美術館にすることは多いのですが、こうも暑いと遠出は無理。
 ということで、この日は比較的近い世田谷美術館へ。「ボストン美術館 華麗なるジャポニスム展」を観覧してきました。

 目当てはモネのラ・ジャポネーズ。確か数年前にも来日したと思うのですが、その時は見逃してしまったので初見です。今回は修復後初公開とのことです。
 芸術新潮の最新号がジャポニスム特集なので、これを読んで予習をしてきたのですが、今作がこんなに巨大な作品だったとは思いませんでした。サイズは231.8×142.3cm。壁一面を占有する形で配置されていて、その部屋に入ると同時にドーンと目に入ってくるので展示効果バツグン! カミーユの着ている着物の赤がとても鮮やか・・これも修復の成果なのでしょうか。見応えありました。

 本展では、「ジャポニスム」作品とその構図のアイデアの元となった浮世絵作品が並べて展示・解説されているので、とても分かりやすかったです。中には「本当にこの絵は浮世絵の影響を受けているの?」と思うような作品もありましたが・・・。

 4年前のボストン美術館展で見た作品も結構ありましたし、浮世絵作品も正月の大浮世絵展他で見たことのある作品が多かったので、真新しさという点では今一でしたが、前述のように展示手法が優れているため十分楽しめました。観覧料は1500円と世田谷美術館にしては高めですが、それも納得。

 しかし、「ボストン美術館展」って頻繁に開催されますね。2年前にも国立博物館でありましたし(この時は日本画でしたが)・・・。この程度の貸し出しでは何の影響もないくらい所蔵品が充実しているということなんでしょうねえ・・・いつか本場を観覧したいものです。
 
 9月15日(月・祝)まで。

ラ・ジャポネーズ
<クロード・モネ>

名所江戸百景 亀戸梅屋敷
<歌川広重>

猫の逢引
<エドゥアール・マネ>

雪に映える朝日、エラニー=シュル=エプト
<カミーユ・ピサロ>

サン=カの港
<ポール・シニャック>

コメント   

 前回は出力対象とする領域の絞り込みをしましたが、まだまだレコード数が多過ぎるので、今回はデータ(=都道府県の輪郭を構成する点)の間引き作業を行います。現レコード数が約260万件ありますが、ポリゴン描画のパフォーマンスを考えると残すのは多くても10万件位となるので、間引きというよりも必要なデータの抽出という表現の方が適していますかね。
 
 データのピックアップで一番単純なのは一定の間隔(10レコード毎、100レコード毎等)で取り出す方法。ただ、前回記述しましたが元データである国土交通省の行政区域のデータ(点)は地域によって点の間隔が異なるようなので、この方法ですと場所によっては点と点の距離が開き過ぎて輪郭が荒くなる可能性があります。
 そこで、輪郭の精度にばらつきが出ないよう、点と点の距離が一定になるように抽出することにしました。
 点と点の距離を緯度・経度から求めるには、以前に試みた「ヒュベニの距離計算式」を利用する方法を今回も適用。ただし、この時に作成したプログラム関数はJavascriptのものなので、これをPHPに変換します。

   //2点間の距離を計算(GRS80)
   //  引数:$p0,$p1 --- 緯度・経度収納の配列   array("Lat"=>latitude,"Lng"=>longitude)
   function getDistance($p0, $p1) {

      $a = 6378137.0;             //長半径
      $f = 1.0 / 298.257222101;   //扁平率
      $b = $a * (1 - $f);           //短半径
      $e2 = (pow($a, 2) - pow($b, 2)) / pow($a, 2);    //第一離心率^2
      $dy = deg2rad($p0["Lat"] - $p1["Lat"]);          //2点の緯度差(ラジアン)
      $dx = deg2rad($p0["Lng"] - $p1["Lng"]);          //2点の経度差(ラジアン)
      $my = deg2rad(($p0["Lat"] + $p1["Lat"]) / 2.0);  //2点の緯度の中間
      $w = sqrt(1.0 - $e2 * pow(sin($my), 2));
      $m = ($a * (1 - $e2)) / pow($w, 3);              //子午線曲率半径
      $n = $a / $w;                                    //卯酉線曲率半径

      return sqrt(pow($dy * $m, 2) + pow($dx * $n * cos($my), 2));

   }

 当関数を使用して一定の距離毎に点を抽出すれば良いのですが、これを領域単位で行うのは問題があります。
 領域データは反時計回りで収納されていますので、東京都と埼玉県の都県境の点を例に挙げますと、東京都側領域データは東から西へ、埼玉県側領域データは逆に西から東へという並び順になっています。そのため、これら2つを別々に処理すると東京都側と埼玉県側とで抽出対象となる点が異なってしまい、地図描画時の輪郭線にズレが生じます。

 試しに1km毎に点を抽出する作業を各領域別々に行ってみたところ・・・日本全図を表示する際には多少のズレが生じても目立たないのですが、ズームしてみると図1-a(左)のように輪郭線が一致していないことがはっきり分かります。

図1-a

図1-a 各領域別々に抽出処理を行った場合
東京都・埼玉県・山梨県の境付近

図1-b

図1-b 輪郭線をグループ化して処理を行った場合
東京都・埼玉県・山梨県の境付近

 このようなズレをなくし図1-b(右)のようにするには、双方の領域において同一の点が抽出されるよう、点を緯度・経度でグループ化して処理する必要があります。
 点のグループ化は以下の関数にて・・・

   //対象の点($point)と同じ緯度・経度の点を探索して配列で返す
   function GroupPointLatLng($mysqli,$point) {

      $strSQL = "SELECT DISTINCT PrefectureCode FROM t_PrefectureBorder";
      $strSQL .= " WHERE Lat = ".$point["Lat"]." AND Lng = ".$point["Lng"];
      $strSQL .= " ORDER BY PrefectureCode";

      $rst = $mysqli->query($strSQL);
      $prefgrp = array();
      while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
         array_push($prefgrp,$col["PrefectureCode"]);
      }
      $rst->close();
      
      return $prefgrp;

   }

 東京都(都道府県コード=13)の領域データを例に挙げると、上記の関数を通すと以下のいずれかの結果が返ります。
 ・海岸線(他県との隣接なし) [0]=>13
 ・埼玉県(都道府県コード=11)との都県境 [0]=>11 [1]=>13
 ・千葉県(都道府県コード=12)との都県境 [0]=>12 [1]=>13
 ・神奈川県(都道府県コード=14)との都県境 [0]=>13 [1]=>14
 ・山梨県(都道府県コード=19)との都県境 [0]=>13 [1]=>19
 ・東京・埼玉・千葉の3都県境点 [0]=>11 [1]=>12 [2]=>13
 ・東京・埼玉・山梨の3都県境点 [0]=>11 [1]=>13 [2]=>19
 ・東京・神奈川・山梨の3都県境点 [0]=>13 [1]=>14 [2]=>19

 クエリは都道府県コード(PrefectureCode)の昇順にて実行していますので(6行目)、配列の先頭の要素には必ず最も小さい番号が収納されます。

 また、抽出作業に先立ち、テーブルにフラグ用フィールドを追加しておきます。
[SQL]
ALTER TABLE t_PrefectureBorder
ADD ExtractFlag TINYINT(2) NOT NULL DEFAULT 0
[/SQL]

 そして以下がメインルーチン

   for ($PrefCode = 1; $PrefCode <= 47; $PrefCode++) {
      echo "Pref=".$PrefCode."<br>";
      echo "  ----- Reading Data -----<br>";
      ob_flush();
      flush();

      $border = array();
      $strSQL = "SELECT t_PrefectureBorder.*";
      $strSQL .= " FROM t_PrefectureBorder";
      $strSQL .= " INNER JOIN t_PrefectureBorderArea";
      $strSQL .= " ON t_PrefectureBorder.PrefectureCode = t_PrefectureBorderArea.PrefectureCode";
      $strSQL .= " AND t_PrefectureBorder.BorderCode = t_PrefectureBorderArea.BorderCode";
      $strSQL .= " WHERE t_PrefectureBorderArea.PrefectureCode = ".$PrefCode;
      $strSQL .= " AND t_PrefectureBorderArea.ActiveFlag = 1";
      $strSQL .= " ORDER BY t_PrefectureBorder.BorderCode, t_PrefectureBorder.PointNo";
      $rst = $mysqli->query($strSQL);
      while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
         array_push($border,$col);
      }
      $rst->close();

      for ($b=0;$b<count($border);$b++) {
         $border[$b]["PrefGrp"] = GroupPointLatLng($mysqli,$border[$b]);
         //ブラウザがタイムアウトしないよう1万件毎に出力
         if ($b % 10000 == 0) {
            echo "   PrefCode=".$border[$b]["PrefectureCode"]."   BorderCode=".$border[$b]["BorderCode"]."  PointNo=".$border[$b]["PointNo"]
                ."  ".$b."<br>";
            ob_flush();
            flush();
         }
      }

      $intervals = 1000;  //点と点の間隔
      echo "  ----- Active Point -----<br>";
      $formerborder = array();
      for ($b=0;$b<count($border);$b++) {
         if (count($border[$b]["PrefGrp"])>0 && $border[$b]["PrefGrp"][0] == $PrefCode) {
            if (!isset($formerborder["BorderCode"]) || $border[$b]["BorderCode"] <> $formerborder["BorderCode"]
                    || count($border[$b]["PrefGrp"]) > count($formerborder["PrefGrp"])
                    || ($b+1 < count($border) && count($border[$b]["PrefGrp"]) > count($border[$b+1]["PrefGrp"]))
                    || getDistance($formerborder,$border[$b]) > $intervals
               )
            {
               $formerborder = $border[$b];

               $strSQL = "UPDATE t_PrefectureBorder";
               $strSQL .= " SET ExtractFlag = 1";
               $strSQL .= " WHERE Lat = " . $border[$b]["Lat"];
               $strSQL .= " AND Lng = " . $border[$b]["Lng"];
               if (!$mysqli->query($strSQL)){
                  echo "失敗!". $strSQL ."<br>";
                  exit();
               }
               //抽出された点の情報をブラウザ出力(タイムアウト対策)
               echo "   PrefCode=".$border[$b]["PrefectureCode"]."   BorderCode=".$border[$b]["BorderCode"]."  PointNo=".$border[$b]["PointNo"]
                      ."  Grp=".implode("-",$border[$b]["PrefGrp"])."<br>";
               ob_flush();
               flush();
            }
         }
      }
   }

 処理は都道府県毎に実行しました。まずは前回に地図出力対象とした領域の点の情報を連想配列$borderに読み込みます(7~20行目)。
 そして、前述の関数GroupPointLatLngを呼び出して、読み込んだ各点についての緯度・経度でグループ化し、$border[“PrefGrp”]に収納(23行目)しています。
 全ての点においてグループ化した後、その情報を元に抽出する点を決めてフラグを更新しています(33~61行目)。対象となる点の抽出条件は37行目~41行目で以下のように指定しています。

 (1)グループ化情報配列($border[“PrefGrp”])の先頭の要素=現在処理中の都道府県コード
  海岸線の場合、配列には1つの都道府県コードしか収納されていないので、当条件は必ず満たします。
  都道府県境の点においては、1点のデータのフラグを処理する際に同一緯度・経度データのフラグも同時に更新するので(61~64行目)、配列の先頭に収納されている場合のみ処理すれば良いことになります。

 (2)次のいずれかの条件を満たしていること 
  A.領域の先頭のレコードである  
  B.グループ化情報配列($border[“PrefGrp”])の要素数が前後のレコードよりも多い

  都道府県境の変わり目では、
   東京都海岸線 [0]=>13 ――― 東京都・神奈川県境 [0]=>13 [1]=>14
   東京都・神奈川県境 [0]=>13 [1]=>14
        ――― 東京都・神奈川県・山梨県の3都県境点 [0]=>13 [1]=>14 [2]=>19
            ――― 東京都・山梨県堺 [0]=>13 [1]=>19

  といった具合に必ず配列の要素数が変わるので、前後のレコードよりも要素数が多い点を抽出することで、
  変わり目を抑えられます。
  C.前回の抽出点からの距離が一定以上である
  前述した関数getDistanceにて距離を測定し、規定の距離($intervals)を超える場合に抽出対象とします。
  今回は、規定距離$intervals=1000mとしましたが(33行目)、この数値を小さくすれば地図の精度が上がり
  ますがデータ数が増える分パフォーマンスは落ち、数値を大きくした場合はその逆となりますので、状況に
  応じて調整します。

 今回の処理の全ソースを以下に記載します。

<?php

   ini_set( 'error_reporting', ~E_WARNING ); 
   ini_set( 'memory_limit', '2048M' );
   set_time_limit(0);
   $errMsg;
   $mysqli = connectDBi($errMsg);
   if ($mysqli) {
      echo "MySQL接続OK<br>";
   } else {
      echo "MySQL接続NG<br>".$errMsg;
      exit();
   }
   echo str_pad(" ",4096)."<br>";

   for ($PrefCode = 1; $PrefCode <= 47; $PrefCode++) {
      echo "Pref=".$PrefCode."<br>";
      echo "  ----- Reading Data -----<br>";
      ob_flush();
      flush();

      $border = array();
      $strSQL = "SELECT t_PrefectureBorder.*";
      $strSQL .= " FROM t_PrefectureBorder";
      $strSQL .= " INNER JOIN t_PrefectureBorderArea";
      $strSQL .= " ON t_PrefectureBorder.PrefectureCode = t_PrefectureBorderArea.PrefectureCode";
      $strSQL .= " AND t_PrefectureBorder.BorderCode = t_PrefectureBorderArea.BorderCode";
      $strSQL .= " WHERE t_PrefectureBorderArea.PrefectureCode = ".$PrefCode;
      $strSQL .= " AND t_PrefectureBorderArea.ActiveFlag = 1";
      $strSQL .= " ORDER BY t_PrefectureBorder.BorderCode, t_PrefectureBorder.PointNo";
      $rst = $mysqli->query($strSQL);
      while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
         array_push($border,$col);
      }
      $rst->close();

      for ($b=0;$b<count($border);$b++) {
         $border[$b]["PrefGrp"] = GroupPointLatLng($mysqli,$border[$b]);
         //ブラウザがタイムアウトしないよう1万件毎に出力
         if ($b % 10000 == 0) {
            echo "   PrefCode=".$border[$b]["PrefectureCode"]."   BorderCode=".$border[$b]["BorderCode"]."  PointNo=".$border[$b]["PointNo"]
                ."  ".$b."<br>";
            ob_flush();
            flush();
         }
      }

      $intervals = 1000;  //点と点の間隔
      echo "  ----- Active Point -----<br>";
      $formerborder = array();
      for ($b=0;$b<count($border);$b++) {
         if (count($border[$b]["PrefGrp"])>0 && $border[$b]["PrefGrp"][0] == $PrefCode) {
            if (!isset($formerborder["BorderCode"]) || $border[$b]["BorderCode"] <> $formerborder["BorderCode"]
                    || count($border[$b]["PrefGrp"]) > count($formerborder["PrefGrp"])
                    || ($b+1 < count($border) && count($border[$b]["PrefGrp"]) > count($border[$b+1]["PrefGrp"]))
                    || getDistance($formerborder,$border[$b]) > $intervals
               )
            {
               $formerborder = $border[$b];

               $strSQL = "UPDATE t_PrefectureBorder";
               $strSQL .= " SET ExtractFlag = 1";
               $strSQL .= " WHERE Lat = " . $border[$b]["Lat"];
               $strSQL .= " AND Lng = " . $border[$b]["Lng"];
               if (!$mysqli->query($strSQL)){
                  echo "失敗!". $strSQL ."<br>";
                  exit();
               }
               //抽出された点の情報をブラウザ出力(タイムアウト対策)
               echo "   PrefCode=".$border[$b]["PrefectureCode"]."   BorderCode=".$border[$b]["BorderCode"]."  PointNo=".$border[$b]["PointNo"]
                      ."  Grp=".implode("-",$border[$b]["PrefGrp"])."<br>";
               ob_flush();
               flush();
            }
         }
      }
   }

   if ($mysqli) {
      $mysqli->close();
   }

   echo "終了しました。<br>";
   ini_restore('error_reporting'); 
   ini_restore('memory_limit');

   //対象の点($point)と同じ緯度・経度の点を探索して配列で返す
   function GroupPointLatLng($mysqli,$point) {

      $strSQL = "SELECT DISTINCT PrefectureCode FROM t_PrefectureBorder";
      $strSQL .= " WHERE Lat = ".$point["Lat"]." AND Lng = ".$point["Lng"];
      $strSQL .= " ORDER BY PrefectureCode";

      $rst = $mysqli->query($strSQL);
      $prefgrp = array();
      while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
         array_push($prefgrp,$col["PrefectureCode"]);
      }
      $rst->close();
      
      return $prefgrp;

   }

   //2点間の距離を計算(GRS80)
   //  引数:$p0,$p1 --- 緯度・経度収納の配列   array("Lat"=>latitude,"Lng"=>longitude)
   function getDistance($p0, $p1) {

      $a = 6378137.0;             //長半径
      $f = 1.0 / 298.257222101;   //扁平率
      $b = $a * (1 - $f);           //短半径
      $e2 = (pow($a, 2) - pow($b, 2)) / pow($a, 2);    //第一離心率^2
      $dy = deg2rad($p0["Lat"] - $p1["Lat"]);          //2点の緯度差(ラジアン)
      $dx = deg2rad($p0["Lng"] - $p1["Lng"]);          //2点の経度差(ラジアン)
      $my = deg2rad(($p0["Lat"] + $p1["Lat"]) / 2.0);  //2点の緯度の中間
      $w = sqrt(1.0 - $e2 * pow(sin($my), 2));
      $m = ($a * (1 - $e2)) / pow($w, 3);              //子午線曲率半径
      $n = $a / $w;                                    //卯酉線曲率半径

      return sqrt(pow($dy * $m, 2) + pow($dx * $n * cos($my), 2));

   }

   //MySQLへ接続
   function connectDBi(&$err) {

     $MySQL_SERVER = "サーバー名";
     $MySQL_USER = "ユーザー名";
     $MySQL_PASSWORD = "パスワード";
     $MySQL_DBNAME = "データベース名";
     $err = "";
     $mysqli = new mysqli($MySQL_SERVER, $MySQL_USER, $MySQL_PASSWORD, $MySQL_DBNAME);
     if ($mysqli->connect_errno) {
        $err = "データベース接続に失敗しました。";
     } else {
        //文字化け対策
        if (!$mysqli->set_charset("utf8")) {
           $err = "文字コードセットに失敗しました。";
        }
     }
     if (strlen($err) > 0) {
        return false;
     } else {
        return $mysqli;
     }
   }

?>

 抽出データ数は3万件ちょっと。これならパフォーマンス的には問題なさそうです。
 このままでも良いのですが、データをより軽くするために抽出データのみを新規テーブルに収納しました。

・テーブル作成
[SQL]
CREATE TABLE t_JapanPrefectureBorder (
PrefectureCode smallint(2) unsigned zerofill NOT NULL,
BorderCode int(5) NOT NULL,
PointNo int(5) NOT NULL DEFAULT ‘0’,
Lat decimal(15,8) DEFAULT NULL,
Lng decimal(15,8) DEFAULT NULL,
PRIMARY KEY (PrefectureCode,BorderCode,PointNo),
KEY LatLng (Lat,Lng)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
[/SQL] 

・今回抽出した点のみを収納

   for ($PrefCode = 1; $PrefCode <= 47; $PrefCode++) {
      echo "PrefCode=".$PrefCode."<br>";
      ob_flush();
      flush();

      $oldBorderCode = array();
      $strSQL = "SELECT DISTINCT BorderCode";
      $strSQL .= " FROM t_PrefectureBorder";
      $strSQL .= " WHERE t_PrefectureBorder.PrefectureCode = ".$PrefCode;
      $strSQL .= " AND t_PrefectureBorder.ExtractFlag = 1";
      $strSQL .= " ORDER BY t_PrefectureBorder.BorderCode";
      $rst = $mysqli->query($strSQL);
      while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
         array_push($oldBorderCode,$col["BorderCode"]);
      }
      $rst->close();

      for ($i=0;$i<count($oldBorderCode);$i++) {
         echo "old bordercode=".$oldBorderCode[$i]."--->  new bordercode=".$i . "<br>";
         $strSQL = "SELECT * FROM t_PrefectureBorder";
         $strSQL .= " WHERE PrefectureCode = ".$PrefCode;
         $strSQL .= " AND BorderCode = ". $oldBorderCode[$i];
         $strSQL .= " AND ExtractFlag = 1";
         $strSQL .= " ORDER BY BorderCode";
         $rst = $mysqli->query($strSQL);
         $point = array();
         while($col = $rst->fetch_array(MYSQLI_ASSOC)) {
            array_push($point,$col);
         }
         $rst->close();

         for ($j=0;$j<count($point);$j++) {
            $strSQL = "INSERT INTO t_JapanPrefectureBorder (PrefectureCode,BorderCode,PointNo,Lat,Lng)";
            $strSQL .= " VALUES (".$PrefCode.",".$i.",".$j.",".$point[$j]["Lat"].",".$point[$j]["Lng"].")";
            if (!$mysqli->query($strSQL)){
               echo "失敗!". $strSQL ."<br>";
               exit();
            }
         }
      }
   }

 このデータをGoogleMapsAPIにてポリゴン表示させたものが下図です(MAP表示)。

図2-a

図2-a 都道府県の輪郭をGoogleMapsAPIにポリゴン出力
<日本全図>

図2-a

図2-b 都道府県の輪郭をGoogleMapsAPIにポリゴン出力
<東京近郊>

 
 拡大しても粗さが目立つようなことはないので、精度は問題なさそうです。次回は、これを操作可能な白地図へと加工します。

コメント