地図について#2

ひつじかいさんの「ひつじかいの雑記帳」というブログを拝見し、その中に、地図の描写において色々と興味深い記事を拝読しました。これは自分でもやってみたい、そう思い、その記録を記載してみたいと思います。

Web上で操作可能な日本の白地図(都道府県別)を作る(2) ― 市区町村境データより都道府県の輪郭を取得 ―

今回の取り組みは、都道府県データを抜き出すことです。ブログにはご苦労されたとの記載があり、その結果をこんな形で楽に学習させていただくことに感謝です。

前回処理でt_pointは1400万件以上データがあるので、都道府県ごとに分割します。PHPソースコードは、ひつじかいさんのままで変更はありません。

<?php
ini_set('error_reporting', ~E_WARNING);
ini_set('memory_limit', '1024M');
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++) {
    $strSQL = "CREATE TABLE IF NOT EXISTS t_Point_Pref" . sprintf("%02d", $PrefCode) . " (";
    $strSQL .= " PrefectureCode smallint(2) unsigned zerofill NOT NULL,";
    $strSQL .= " AdministrativeAreaCode int(5) unsigned zerofill NOT NULL DEFAULT '00000',";
    $strSQL .= " SurfaceCode int(5) NOT NULL,";
    $strSQL .= " CurveCode int(5) NOT NULL,";
    $strSQL .= " PointNo int(5) NOT NULL DEFAULT '0',";
    $strSQL .= " Lat decimal(15,8) DEFAULT NULL,";
    $strSQL .= " Lng decimal(15,8) DEFAULT NULL,";
    $strSQL .= " CityBorderFlag tinyint(2) NOT NULL DEFAULT '0',";
    $strSQL .= " PRIMARY KEY (PrefectureCode,AdministrativeAreaCode,SurfaceCode,CurveCode,PointNo),";
    $strSQL .= " UNIQUE (SurfaceCode,CurveCode,PointNo),";
    $strSQL .= " KEY LatLng (Lat,Lng)";
    $strSQL .= ") ENGINE=MyISAM DEFAULT CHARSET=utf8";
    if (!$mysqli->query($strSQL)) {
        echo "失敗!" . $strSQL . "<br>";
        exit();
    }
    echo "Create Table : t_Point_Pref" . sprintf("%02d", $PrefCode) . "<br>";
    ob_flush();
    flush();

    $strSQL = "INSERT INTO t_Point_Pref" . sprintf("%02d", $PrefCode) . " (PrefectureCode,AdministrativeAreaCode,SurfaceCode,CurveCode,PointNo,Lat,Lng)";
    $strSQL .= " SELECT PrefectureCode,AdministrativeAreaCode,SurfaceCode,CurveCode,PointNo,Lat,Lng";
    $strSQL .= " FROM t_Point WHERE PrefectureCode = " . $PrefCode;
    if (!$mysqli->query($strSQL)) {
        echo "失敗!" . $strSQL . "<br>";
        exit();
    }
    echo "INSERT DATA TO : t_Point_Pref" . sprintf("%02d", $PrefCode) . "<br>";
    ob_flush();
    flush();
}

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

echo "終了しました。<br>";

ini_restore('error_reporting');
ini_restore('memory_limit');

//MySQLへ接続
function connectDBi(&$err)
{
    //MySQL 接続情報
    $MySQL_SERVER = "localhost";
    $MySQL_USER = "[user_name]";
    $MySQL_PASSWORD = "[password]";
    $MySQL_DBNAME = "map";
    $err = "";
    $mysqli = new mysqli($MySQL_SERVER, $MySQL_USER, $MySQL_PASSWORD, $MySQL_DBNAME);
    if ($mysqli->connect_errno) {
        $err = "データベース接続に失敗しました。";
    }
    if (strlen($err) > 0) {
        return false;
    } else {
        return $mysqli;
    }
}

そして次はこのCityBorderFlagの更新処理。同一都道府県内の市区町村境界であるか否かのチェックです。

<?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++) {
    $strSQL = "UPDATE t_Point_Pref".sprintf("%02d",$PrefCode);
    $strSQL .= " INNER JOIN t_Point_Pref".sprintf("%02d",$PrefCode)." AS t_Point_LatLng";
    $strSQL .= " ON t_Point_Pref".sprintf("%02d",$PrefCode).".Lat = t_Point_LatLng.Lat";
    $strSQL .= " AND t_Point_Pref".sprintf("%02d",$PrefCode).".Lng = t_Point_LatLng.Lng";
    $strSQL .= " AND (t_Point_Pref".sprintf("%02d",$PrefCode).".SurfaceCode <> t_Point_LatLng.SurfaceCode";
    $strSQL .= " OR t_Point_Pref".sprintf("%02d",$PrefCode).".CurveCode <> t_Point_LatLng.CurveCode)";
    $strSQL .= " SET t_Point_Pref".sprintf("%02d",$PrefCode).".CityBorderFlag = 1";
    if (!$mysqli->query($strSQL)){
       echo "失敗!". $strSQL ."<br>";
       exit();
    }
    echo "UPDATE CityBorderFlag : t_Point_Pref".sprintf("%02d",$PrefCode)."<br>";
    ob_flush();
    flush();
}

//MySQLへ接続
function connectDBi(&$err)
{
    //MySQL 接続情報
    $MySQL_SERVER = "localhost";
    $MySQL_USER = "[user_name]";
    $MySQL_PASSWORD = "[password]";
    $MySQL_DBNAME = "map";
    $err = "";
    $mysqli = new mysqli($MySQL_SERVER, $MySQL_USER, $MySQL_PASSWORD, $MySQL_DBNAME);
    if ($mysqli->connect_errno) {
        $err = "データベース接続に失敗しました。";
    }
    if (strlen($err) > 0) {
        return false;
    } else {
        return $mysqli;
    }
}

次に都道府県境用のテーブルを用意します。

 ・t_PrefectureBorder
[SQL]
CREATE TABLE IF NOT EXISTS `t_PrefectureBorder` (
    `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]

そして都道府県データの生成です。こちらも、ひつじかいさんのソースそのままです。

<?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++) {
    $strSQL = "SELECT t_Point_Pref" . sprintf("%02d", $PrefCode) . ".* FROM t_Point_Pref" . sprintf("%02d", $PrefCode);
    $strSQL .= " INNER JOIN";
    $strSQL .= " (SELECT PrefectureCode, AdministrativeAreaCode, SurfaceCode, CurveCode, Min(CityBorderFlag), Max(CityBorderFlag)";
    $strSQL .= " FROM t_Point_Pref" . sprintf("%02d", $PrefCode);
    $strSQL .= " GROUP BY PrefectureCode, AdministrativeAreaCode, SurfaceCode, CurveCode";
    $strSQL .= " HAVING Min(CityBorderFlag)=0 AND Max(CityBorderFlag)=1) AS q_BorderCurve";
    $strSQL .= " ON t_Point_Pref" . sprintf("%02d", $PrefCode) . ".PrefectureCode = q_BorderCurve.PrefectureCode";
    $strSQL .= " AND t_Point_Pref" . sprintf("%02d", $PrefCode) . ".AdministrativeAreaCode = q_BorderCurve.AdministrativeAreaCode";
    $strSQL .= " AND t_Point_Pref" . sprintf("%02d", $PrefCode) . ".SurfaceCode = q_BorderCurve.SurfaceCode";
    $strSQL .= " AND t_Point_Pref" . sprintf("%02d", $PrefCode) . ".CurveCode = q_BorderCurve.CurveCode";
    $strSQL .= " ORDER BY SurfaceCode, CurveCode, PointNo";
    echo "Pref=" . $PrefCode . "<br>";
    echo "  ----- Reading Point Data -----<br>";
    ob_flush();
    flush();

    $rst = $mysqli->query($strSQL);
    $rcount = $rst->num_rows;

    $pointInfo = array();
    $connectpoint = array();
    $i = 0;
    $j = 0;
    $formerpoint = array();

    if ($rcount > 0) {
        while ($col = $rst->fetch_array(MYSQLI_ASSOC)) {
            if (
                isset($formerpoint["PrefectureCode"]) && $formerpoint["PrefectureCode"] == $PrefCode
                && ($formerpoint["SurfaceCode"] <> $col["SurfaceCode"] || $formerpoint["CurveCode"] <> $col["CurveCode"])
            ) {
                $i++;
                $j = 0;
            }
            $col["iNo"] = $i;
            $col["jNo"] = $j;
            $col["CheckedFlagP"] = 0;
            $col["CheckedFlagS"] = 0;
            $pointInfo[$i][$j] = $col;
            if ($j > 0) {
                if (!$pointInfo[$i][$j - 1]["CityBorderFlag"] && $pointInfo[$i][$j]["CityBorderFlag"]) {
                    array_push($connectpoint, $pointInfo[$i][$j]);
                } else if ($pointInfo[$i][$j - 1]["CityBorderFlag"] && !$pointInfo[$i][$j]["CityBorderFlag"]) {
                    array_push($connectpoint, $pointInfo[$i][$j - 1]);
                }
            }
            $formerpoint = $col;
            $j++;
        }
    }

    $border = array();
    $formerp = Null;
    $p = get_BorderStartPoint($pointInfo);
    $bNo = 0;
    while (is_array($p)) {
        if (!isset($border[$bNo])) {
            $border[$bNo] = array();
        }
        if (!isset($formerp) || $p["Lat"] <> $formerp["Lat"] || $p["Lng"] <> $formerp["Lng"]) {
            array_push($border[$bNo], $p);  //前の点と同じ緯度経度の場合は登録しない
        }
        $pointInfo[$p["iNo"]][0]["CheckedFlagS"] = 1;         //チェック(領域単位)
        $pointInfo[$p["iNo"]][$p["jNo"]]["CheckedFlagP"] = 1; //チェック(ポイント単位)
        $formerp = $p;
        $p = get_NextPoint($pointInfo, $connectpoint, $p);
        if (is_null($p)) {
            $p = get_BorderStartPoint($pointInfo);
            $bNo++;
        }
    }

    for ($i = 0; $i < count($border); $i++) {
        //開始点と終了点の座標が同じになるよう調整
        if (count($border[$i]) > 1) {
            if (
                $border[$i][0]["Lat"] <> $border[$i][count($border[$i]) - 1]["Lat"]
                || $border[$i][0]["Lng"] <> $border[$i][count($border[$i]) - 1]["Lng"]
            ) {
                array_push($border[$i], $border[$i][0]);
            }
        }
        echo "INSERT to t_PrefectureBorder: BorderCode=" . $i . "  record count:" . count($border[$i]) . "<br>";
        ob_flush();
        flush();
        for ($j = 0; $j < count($border[$i]); $j++) {
            $strSQL = "INSERT INTO t_PrefectureBorder (PrefectureCode,BorderCode,PointNo,Lat,Lng)";
            $strSQL .= " VALUES (" . $PrefCode . "," . $i . "," . $j . ",";
            $strSQL .= $border[$i][$j]["Lat"] . "," . $border[$i][$j]["Lng"] . ")";
            if (!$mysqli->query($strSQL)) {
                echo "失敗!" . $strSQL . "<br>";
                exit();
            }
        }
    }

    //境界線が全て都道府県境となる領域(島など)
    $strSQL = "SELECT PrefectureCode, AdministrativeAreaCode, SurfaceCode, CurveCode, Min(CityBorderFlag), Max(CityBorderFlag)";
    $strSQL .= " FROM t_Point_Pref" . sprintf("%02d", $PrefCode);
    $strSQL .= " GROUP BY PrefectureCode, AdministrativeAreaCode, SurfaceCode, CurveCode";
    $strSQL .= " HAVING Max(CityBorderFlag)=0";
    $rst = $mysqli->query($strSQL);
    $rcount = $rst->num_rows;

    if ($rcount > 0) {
        $bNo = count($border);
        while ($col = $rst->fetch_array(MYSQLI_ASSOC)) {
            $strSQL = "INSERT INTO t_PrefectureBorder (PrefectureCode,BorderCode,PointNo,Lat,Lng)";
            $strSQL .= " SELECT PrefectureCode," . $bNo . ",PointNo,Lat,Lng";
            $strSQL .= " FROM t_Point_Pref" . sprintf("%02d", $PrefCode);
            $strSQL .= " WHERE PrefectureCode = " . $col["PrefectureCode"];
            $strSQL .= " AND AdministrativeAreaCode = " . $col["AdministrativeAreaCode"];
            $strSQL .= " AND SurfaceCode = " . $col["SurfaceCode"];
            $strSQL .= " AND CurveCode = " . $col["CurveCode"];
            echo "INSERT to t_PrefectureBorder: BorderCode=" . $bNo . " (islands and others)<br>";
            ob_flush();
            flush();
            if (!$mysqli->query($strSQL)) {
                echo "失敗!" . $strSQL . "<br>";
                exit();
            }
            $bNo++;
        }
    }
}

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

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

//輪郭の始点となる点を設定
function get_BorderStartPoint($pointInfo)
{
    echo " ----- Get Border Start Point -----<br>";
    ob_flush();
    flush();
    for ($i = 0; $i < count($pointInfo); $i++) {
        if (!$pointInfo[$i][0]["CheckedFlagS"]) {  //チェック済みの点のある領域がスタート地点になることはない
            for ($j = 0; $j < count($pointInfo[$i]); $j++) {
                if (!$pointInfo[$i][$j]["CityBorderFlag"]) {
                    //出力
                    foreach ($pointInfo[$i][$j] as $key => $value) {
                        echo $value . " ";
                    }
                    echo "<br>";
                    ob_flush();
                    flush();
                    return $pointInfo[$i][$j];
                }
            }
        }
    }
    echo "      not found<br>";
    ob_flush();
    flush();
    return NULL; //見つからない(=全ての点をチェックした)場合
}

//都道府県輪郭線の次の点を探す
function get_NextPoint($pointInfo, $connectpoint, $p)
{
    $i = $p["iNo"];
    $j = $p["jNo"] + 1;
    if ($j >= count($pointInfo[$i])) {
        $j = 0;
    }
    if (!$pointInfo[$i][$j]["CheckedFlagP"]) {
        if (!$pointInfo[$i][$j]["CityBorderFlag"]) {
            return $pointInfo[$i][$j];
        } else {
            return get_ConnectCityBorder($connectpoint, $pointInfo[$i][$j]);
        }
    } else {
        return NULL; //チェック済みの点に戻ってきた(=1周した)
    }
}

//市区町村の変わり目(接続点)を取得
function get_ConnectCityBorder($connectpoint, $p)
{
    echo "----- Connect City Border -----<br>";
    $cp = array();
    for ($c = 0; $c < count($connectpoint); $c++) {
        if ($connectpoint[$c]["Lat"] == $p["Lat"] && $connectpoint[$c]["Lng"] == $p["Lng"]) {
            array_push($cp, $connectpoint[$c]);
        }
    }
    //3つ以上の市町村境である点を考慮し、接続元の市区町村の次に登録されている点を返す
    if (count($cp) > 0) {
        $chk = 0;
        $cc = 0;
        for ($c = 0; $c < count($cp); $c++) {
            if ($cp[$c]["iNo"] == $p["iNo"]) {
                $chk = 1;
            } else {
                if ($chk == 1) {
                    $cc = $c;
                    break;
                }
            }
        }
        //出力
        foreach ($cp[$cc] as $key => $value) {
            echo $value . " ";
        }
        echo "<br>";
        ob_flush();
        flush();
        return $cp[$cc];
    }
    return NULL; //見つからなかった場合(通常はあり得ないはず)
}

//MySQLへ接続
function connectDBi(&$err)
{
    //MySQL 接続情報
    $MySQL_SERVER = "localhost";
    $MySQL_USER = "*****";
    $MySQL_PASSWORD = "*****";
    $MySQL_DBNAME = "map";
    $err = "";
    $mysqli = new mysqli($MySQL_SERVER, $MySQL_USER, $MySQL_PASSWORD, $MySQL_DBNAME);
    if ($mysqli->connect_errno) {
        $err = "データベース接続に失敗しました。";
    }
    if (strlen($err) > 0) {
        return false;
    } else {
        return $mysqli;
    }
}

ソースコードの解説については、ひつじかいさんのブログで詳しく記載されています。また、ひつじかいさんのWEBソースコード(HTML/JavaScript)を拝借し、PHP部分は自分で記載してみました。ちゃんと描けているようです。

データ数の多い、長崎県・鹿児島県のデータも表示させてみました。確かに多少待たされますが、特に大きな問題はないようです(長崎県は見た目、本土と諸島との面積は変わらないくらいなのですね)。

ひつじかいさんの行間に含まれるご苦労を感じながら、ありがたく学ばせていただいています。