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


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


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

