地図について#1

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

ただ記事は2014年の記載ということもあり、若干ですが、その後に公開されている地図情報の仕様が変更されたり、それにより公開されているプログラムがそのまま動作しないところもあるようです。

ひつじかいさんの情報を学ぶため、仕様変更点などを加味し、記載していきたいと思います。

Web上で操作可能な日本の白地図(都道府県別)を作る(1) ― 行政区域データの取得・加工 ―

こちらの情報では、国土交通省の国土数値情報をダウンロードし、地図を描写することが記載されています。

データはSHAPE形式、XML形式に加え、GeoJSONファイルもあるようです。ここでは、ひつじかいさんの内容に従い、XML形式のファイルを扱いたいと思います。そのXMLファイルについて確認します。

行政区域情報ですが、内容が若干異なっているようです。

◆2020年5月時点◆
<ksj:AdministrativeBoundary gml:id="gy32287">
    <ksj:bounds xlink:href="#sf32287"/>
    <ksj:prefectureName>東京都</ksj:prefectureName>
    <ksj:subPrefectureName>
    <ksj:countyName></ksj:countyName>
    <ksj:cityName>府中市</ksj:cityName>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13206</ksj:administrativeAreaCode>
<ksj:AdministrativeBoundary>
◆ひつじかいさんからの引用◆
<ksj:AdministrativeArea gml:id="gy40"> <!-- ID -->
    <ksj:are xlink:href="#sf40"/>      <!-- 対応する「領域面データ」(2)のID -->
    <ksj:prn>東京都</ksj:prn>          <!-- 都道府県名 -->
    <ksj:sun></ksj:sun>                <!-- 支庁名(北海道のみ)-->
    <ksj:con></ksj:con>                <!-- 郡・政令市名 -->
    <ksj:cn2>府中市</ksj:cn2>          <!-- 市区町村名 -->
    <ksj:aac codeSpace="AdministrativeAreaCode.xml">13206</ksj:aac> <!-- 行政区域コード -->
</ksj:AdministrativeArea>

次に領域面データですが、こちらは変更ないようです。

◆2020年5月時点◆
<gml:Surface gml:id="sf32287">
	<gml:patches>
		<gml:PolygonPatch>
			<gml:exterior>
				<gml:Ring>
					<gml:curveMember xlink:href="#cv32287_0"/>
				</gml:Ring>
			</gml:exterior>
		</gml:PolygonPatch>
	</gml:patches>
</gml:Surface>
◆ひつじかいさんからの引用◆
<gml:Surface gml:id="sf40">
    <gml:patches>
         <gml:PolygonPatch>
             <gml:exterior>
                 <gml:Ring>
                    <gml:curveMember xlink:href="#cv40_0"/> <!-- 対応する「境界線データ」(3)のID -->
                </gml:Ring>
             </gml:exterior>
         </gml:PolygonPatch>
     </gml:patches>
 </gml:Surface>

境界線データについて、こちらも変更はないようです。

◆2020年5月時点◆
<gml:Curve gml:id="cv32287_0">
	<gml:segments>
		<gml:LineStringSegment>
			<gml:posList>
			35.68725259 139.49718667
                          :(略)
			35.68725259 139.49718667
			</gml:posList>
		</gml:LineStringSegment>
	</gml:segments>
</gml:Curve>
◆ひつじかいさんからの引用◆
<gml:Curve gml:id="cv40_0"> <!-- 境界線データID -->
    <gml:segments>
        <gml:LineStringSegment>
            <gml:posList>
            35.69545100 139.48958400
            35.69537900 139.48936800
                    (中略)
            35.69535200 139.48964000
            35.69545100 139.48958400
            </gml:posList>
        </gml:LineStringSegment>
    </gml:segments>
</gml:Curve>

ここでローカルに各種テーブルを作成します。データベース名は[map]にしたいと思います。ひつじかいさんの情報に従って、次のSQLを実行させ、テーブルを作成していきます。

・t_Prefecture 都道府県情報
[SQL]
CREATE TABLE IF NOT EXISTS `t_Prefecture` (
    `PrefectureCode` smallint(2) unsigned zerofill NOT NULL, # 都道府県コード
    `PrefectureName` varchar(10) NOT NULL, # 都道府県名
    PRIMARY KEY (`PrefectureCode`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
[/SQL]

・t_AdministrativeArea 行政区域(市区町村)情報
[SQL]
CREATE TABLE IF NOT EXISTS `t_AdministrativeArea` (
    `PrefectureCode` smallint(2) unsigned zerofill NOT NULL, # 都道府県コード
    `AdministrativeAreaCode` int(5) unsigned zerofill NOT NULL, # 行政区域コード(全国地方公共団体コード)
    `CityName` varchar(100) DEFAULT NULL, # 市区町村名
    `SubprefectureName` varchar(100) DEFAULT NULL, # 支庁名(北海道のみ)
PRIMARY KEY (`PrefectureCode`,`AdministrativeAreaCode`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
[/SQL]

・t_Surface 領域面情報
[SQL]
CREATE TABLE IF NOT EXISTS `t_Surface` (
    `PrefectureCode` smallint(2) unsigned zerofill NOT NULL, # 都道府県コード
    `AdministrativeAreaCode` int(5) unsigned zerofill DEFAULT NULL, # 行政区域コード(全国地方公共団体コード)
    `SurfaceCode` int(5) NOT NULL, # 領域面コード
PRIMARY KEY (`PrefectureCode`,`AdministrativeAreaCode`,`SurfaceCode`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
[/SQL]

・t_Curve 境界線情報
[SQL]
CREATE TABLE IF NOT EXISTS `t_Curve` (
    `PrefectureCode` smallint(2) unsigned zerofill NOT NULL, # 都道府県コード
    `AdministrativeAreaCode` int(5) unsigned zerofill DEFAULT NULL, # 行政区域コード(全国地方公共団体コード)
    `SurfaceCode` int(5) NOT NULL, # 領域面コード
    `CurveCode` int(5) NOT NULL, # 境界線コード
    `InteriorFlag` tinyint(2) NOT NULL DEFAULT 0, # 通常(外輪線)は=0 内輪腺の場合>0
PRIMARY KEY (`PrefectureCode`,`AdministrativeAreaCode`,`SurfaceCode`,`CurveCode`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
[/SQL]

・t_Point 点情報
[SQL]
CREATE TABLE IF NOT EXISTS `t_Point` (
    `PrefectureCode` smallint(2) unsigned zerofill NOT NULL, # 都道府県コード
    `AdministrativeAreaCode` int(5) unsigned zerofill DEFAULT NULL, # 行政区域コード(全国地方公共団体コード)
    `SurfaceCode` int(5) NOT NULL, # 領域面コード
    `CurveCode` 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`,`AdministrativeAreaCode`,`SurfaceCode`,`CurveCode`,`PointNo`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
[/SQL]

参考まで、ダンプファイル生成と復元のWindowsにおけるコマンドは次の通りで、生成したdumpファイルはこちら

[生成] mysqldump --single-transaction -u [user_name] -p map > c:\temp\map1.dump
[復元] mysql -u [user_name] -p map < c:\temp\map1.dump

次に作成したDBに対して、XMLファイルを読み込み書き込んでいきます。実行させるPHPですが、XMLファイルの変更を踏まえ、次の通り、記載し直しました。データはサブフォルダ[pref]に入れてあります。

<?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++) {
    $FileName = "pref/N03-19_" . sprintf("%02d",$PrefCode) . "_190101.xml";      //XMLファイル名
    $xmlString = file_get_contents($FileName);
    $xmlString = preg_replace('/:/', '_', $xmlString);
    $xmlData = simplexml_load_string($xmlString);

    $areaInfo = array();
    $aNo = 0;
    $surfaceInfo = array();
    $sNo = 0;
    foreach ($xmlData->ksj_AdministrativeBoundary as $data) {
        if (isset($data->ksj_administrativeAreaCode)) {
            if (strlen((string) $data->ksj_administrativeAreaCode) == 0) {
                $areaCode = intval($PrefCode . "000");
            } else {
                $areaCode = (int) $data->ksj_administrativeAreaCode;
            }
            if (!existAreaCode($areaInfo, $areaCode)) {
                $areaInfo[$aNo] = new AdministrativeArea;
                $areaInfo[$aNo]->AreaCode = $areaCode;
                $areaInfo[$aNo]->Prf = (string) $data->ksj_prefectureName;
                $areaInfo[$aNo]->Sun = (string) $data->ksj_subPrefectureName;
                $areaInfo[$aNo]->Con = (string) $data->ksj_countyName;
                $areaInfo[$aNo]->Cty = (string) $data->ksj_cityName;
                $aNo++;
            }
            $surfaceInfo[$sNo] = new Surface;
            $surfaceInfo[$sNo]->AreaCode = $areaCode;
            $surfaceInfo[$sNo]->SurfaceCode = (int) str_replace("#sf", "", $data->ksj_bounds['xlink_href']);
            $sNo++;
        }
    }

    $curveInfo = array();
    $cNo = 0;
    foreach ($xmlData->gml_Surface as $gmlsf) {
        if (isset($gmlsf['gml_id'])) {
            $intSurface = (int) str_replace("sf", "", $gmlsf['gml_id']);
            $areaCode = get_AreaCode($surfaceInfo, $intSurface);
            foreach ($gmlsf->xpath('gml_patches//gml_Ring/gml_curveMember') as $gmlcv) {
                if (isset($gmlcv['xlink_href'])) {
                    $curveInfo[$cNo] = new Curve;
                    $curveInfo[$cNo]->AreaCode = $areaCode;
                    $curveInfo[$cNo]->SurfaceCode = $intSurface;
                    $curveInfo[$cNo]->CurveCode = (int) substr($gmlcv['xlink_href'], strpos($gmlcv['xlink_href'], "_") + 1);
                    $curveInfo[$cNo]->CurveID = (string) str_replace("#cv", "", $gmlcv['xlink_href']);
                    $cNo++;
                }
            }
        }
    }

    $pointInfo = array();
    $pNo = 0;
    foreach ($xmlData->gml_Curve as $gmlcv) {
        if (isset($gmlcv['gml_id'])) {
            $strCurveID = (string) str_replace("cv", "", $gmlcv['gml_id']);
            $intCurve = (int) substr($strCurveID, strpos($strCurveID, "_") + 1);
            $intSurface = get_SurfaceCode($curveInfo, $strCurveID);
            $areaCode = get_AreaCode($surfaceInfo, $intSurface);
            foreach ($gmlcv->gml_segments->gml_LineStringSegment->gml_posList as $posList) {
                $strlatlng = explode("\n", trim($posList));
                for ($pp = 0; $pp < count($strlatlng); $pp++) {
                    $latlng = explode(" ", trim($strlatlng[$pp]));
                    if (count($latlng) == 2) {
                        if (is_numeric($latlng[0]) && is_numeric($latlng[1])) {
                            $pointInfo[$pNo] = new Point;
                            $pointInfo[$pNo]->AreaCode = $areaCode;
                            $pointInfo[$pNo]->SurfaceCode = $intSurface;
                            $pointInfo[$pNo]->CurveCode = $intCurve;
                            $pointInfo[$pNo]->PointCode = $pp;
                            $pointInfo[$pNo]->Lat = $latlng[0];
                            $pointInfo[$pNo]->Lng = $latlng[1];
                            $pNo++;
                        }
                    }
                }
            }
        }
    }

    echo "Pref=" . $PrefCode . "<br>";
    echo "area count = " . count($areaInfo) . "<br>";
    echo "surface count = " . count($surfaceInfo) . "<br>";
    echo "curve count = " . count($curveInfo) . "<br>";
    echo "point count = " . count($pointInfo) . "<br>";
    ob_flush();
    flush();

    $strSQL = "INSERT INTO t_Prefecture (`PrefectureCode`,`PrefectureName`)";
    $strSQL .= " VALUES (" . $PrefCode . ",'" . $areaInfo[0]->Prf . "')";
    if ($mysqli) {
        if (!$mysqli->query($strSQL)) {
            echo "失敗!" . $strSQL . "<br>";
            exit();
        }
    }

    for ($i = 0; $i < count($areaInfo); $i++) {
        $strSQL = "INSERT INTO t_AdministrativeArea (`PrefectureCode`,`AdministrativeAreaCode`,`CityName`,`SubprefectureName`)";
        $strSQL .= " VALUES (" . $PrefCode . "," . $areaInfo[$i]->AreaCode . ",'" . $areaInfo[$i]->Con . $areaInfo[$i]->Cty . "','" . $areaInfo[$i]->Sun . "')";
        if ($mysqli) {
            if (!$mysqli->query($strSQL)) {
                echo "失敗!" . $strSQL . "<br>";
                exit();
            }
        }
    }

    for ($i = 0; $i < count($surfaceInfo); $i++) {
        $strSQL = "INSERT INTO t_Surface (`PrefectureCode`,`AdministrativeAreaCode`,`SurfaceCode`)";
        $strSQL .= " VALUES (" . $PrefCode . "," . $surfaceInfo[$i]->AreaCode . "," . $surfaceInfo[$i]->SurfaceCode . ")";
        if ($mysqli) {
            if (!$mysqli->query($strSQL)) {
                echo "失敗!" . $strSQL . "<br>";
                exit();
            }
        }
    }

    for ($i = 0; $i < count($curveInfo); $i++) {
        $strSQL = "INSERT INTO t_Curve (`PrefectureCode`,`AdministrativeAreaCode`,`SurfaceCode`,`CurveCode`)";
        $strSQL .= " VALUES (" . $PrefCode . "," . $curveInfo[$i]->AreaCode . "," . $curveInfo[$i]->SurfaceCode . "," . $curveInfo[$i]->CurveCode . ")";
        if ($mysqli) {
            if (!$mysqli->query($strSQL)) {
                echo "失敗!" . $strSQL . "<br>";
                exit();
            }
        }
    }

    for ($i = 0; $i < count($pointInfo); $i++) {
        $strSQL = "INSERT INTO t_Point (`PrefectureCode`,`AdministrativeAreaCode`,`SurfaceCode`,`CurveCode`,`PointNo`,`Lat`,`Lng`)";
        $strSQL .= " VALUES (" . $PrefCode . "," . $pointInfo[$i]->AreaCode . "," . $pointInfo[$i]->SurfaceCode . "," . $pointInfo[$i]->CurveCode;
        $strSQL .= "," . $pointInfo[$i]->PointCode . "," . $pointInfo[$i]->Lat . "," . $pointInfo[$i]->Lng . ")";
        if ($mysqli) {
            if (!$mysqli->query($strSQL)) {
                echo "失敗!" . $strSQL . "<br>";
                exit();
            }
        }
    }
}

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

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

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

function get_AreaCode($surfaceinfo, $sfCode)
{
    for ($i = 0; $i < count($surfaceinfo); $i++) {
        if ($surfaceinfo[$i]->SurfaceCode == $sfCode) {
            return $surfaceinfo[$i]->AreaCode;
        }
    }
    return null;
}

function get_SurfaceCode($curveinfo, $cvID)
{
    for ($i = 0; $i < count($curveinfo); $i++) {
        if ($curveinfo[$i]->CurveID == $cvID) {
            return $curveinfo[$i]->SurfaceCode;
        }
    }
    return null;
}

function get_cvNo($cvCode, $curveinfo)
{
    $strCurve = (string) str_replace("cv", "", $cvCode);
    $intCurve = (int) substr($strCurve, 0, strpos($strCurve, "_"));
    $intCurveSub = (int) substr($strCurve, strpos($strCurve, "_") + 1);
    for ($i = 0; $i < count($curveinfo); $i++) {
        if ($curveinfo[$i]->CurveCode == $intCurve && $curveinfo[$i]->CurveCodeSub == $intCurveSub) {
            return $i;
        }
    }
    return -1;
}

function existAreaCode($areaInfo, $areaCode)
{
    for ($i = 0; $i < count($areaInfo); $i++) {
        if ($areaInfo[$i]->AreaCode == $areaCode) {
            return true;
        }
    }
    return false;
}

//行政区域(市区町村)情報 
class AdministrativeArea
{
    public $AreaCode;    //行政区域コード(全国地方公共団体コード)
    public $Prf;         //都道府県名
    public $Sun;         //支庁名(北海道のみ)
    public $Con;         //郡・政令市名
    public $Cty;         //市区町村名
}

//領域面情報
class Surface
{
    public $AreaCode;        //行政区域コード(全国地方公共団体コード)
    public $SurfaceCode = 0; //領域面コード
}

//境界線情報
class Curve
{
    public $AreaCode;        //行政区域コード(全国地方公共団体コード)
    public $SurfaceCode = 0; //領域面コード
    public $CurveCode = 0;   //境界線コード  外輪線=0 内輪腺>0
    public $CurveID;         //境界線ID(照合用文字列)
}

//点情報
class Point
{
    public $AreaCode;        //行政区域コード(全国地方公共団体コード)
    public $SurfaceCode = 0; //領域面コード
    public $CurveCode = 0;   //境界線コード
    public $PointCode = 0;   //点コード
    public $Lat;             //緯度
    public $Lng;             //経度
}

//MySQLへ接続
function connectDBi(&$err) {
    //MySQL 接続情報
    $MySQL_SERVER = "localhost";
    $MySQL_USER = [user_name];
    $MySQL_PASSWORD = [user_password];
    $MySQL_DBNAME = "map";

    $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;
    }
}

データ処理には少し時間が掛かります。途中経過はブラウザに表示されるのでじっと待つことにします。正確には測っていませんが、40分ほど処理に要しました。

演算が終了したら、実際にデータを描かせようと思います。ひつじかいさんのWEBソースコード(HTML/JavaScript)を拝借し、PHP部分は自分で記載してみました。ちゃんと描けているようです。