OpenLayers : してログ

XYZ 方式のタイルレイヤで、用意されていないズームレベルのタイルを、それより低い解像度のタイルで代用したいときがあります。地理院地図で細かい地図を表示したくないときや、容量やレンダリング負荷を減らしたい場合に有効なテクニックです。OpenLayers2 は不可能だと思って PHP で対応していたのですが、今回可能だということが分かったので紹介します。

OpenLayers2

serverResolutions にサーバーでサポートする解像度を配列で指定しておくと、それ以外のズームレベルで拡大表示が行われます。解像度は1ピクセル当たりのメートル単位になっており、下記ソースコードの配列で0~18の順で並んでいます。拡大表示を確認するには、下側の解像度をいくつかコメントアウトしてみてください。

var map = new OpenLayers.Map({
	div: "map",
	projection: "EPSG:3857",
	displayProjection: "EPSG:4326",
	numZoomLevels: 19,
	layers: [
		new OpenLayers.Layer.XYZ(
			"GSI Maps", 
			"https://cyberjapandata.gsi.go.jp/xyz/std/${z}/${x}/${y}.png",
			{
				serverResolutions: [
					156543.03390625,
					78271.516953125,
					39135.7584765625,
					19567.87923828125,
					9783.939619140625,
					4891.9698095703125,
					2445.9849047851562,
					1222.9924523925781,
					611.4962261962891,
					305.74811309814453,
					152.87405654907226,
					76.43702827453613,
					38.218514137268066
					19.109257068634033,
					9.554628534317017,
					4.777314267158508,
					2.388657133579254
					1.194328566789627,
					0.5971642833948135
				]
			}
		)
	],
	center: new OpenLayers.LonLat(139, 37).transform("EPSG:4326", "EPSG:3857"),
	zoom: 5
});
OpenLayers3

maxZoom にサーバーでサポートするズームレベルを指定すると、それ以外のズームレベルで拡大表示が行われます。下記ソースコードでは、ズームレベル11以上は10の拡大表示になります。

var map = new ol.Map({
	target: 'map',
	layers: [
		new ol.layer.Tile({
			source: new ol.source.XYZ({
				url: "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png",
				maxZoom: 10
			})
		})
	],
	view: new ol.View({
		center: ol.proj.fromLonLat([139, 37]),
		zoom: 5
	})
});
Leaflet

Leaflet では maxNativeZoom を指定することで、同様の拡大表示を実現可能です。

PHP で無理やり対応(おまけ)

OpenLayers2 にはそういった機能が無いと思って、PHP で動的に拡大カットするラッパーを組んでしまいました。必要性が無くなったものの、他にも応用が効くコードかと思いますので公開しておきます。

  • シンプルにするために、以下のコードでは入力パラメータをチェックしていませんのでご注意ください
  • $max_zoom にはタイルレイヤで用意されている最大ズームレベルを入れてください
$z = $_REQUEST['z'];
$x = $_REQUEST['x'];
$y = $_REQUEST['y'];

$max_zoom = 12;

if ($z > $max_zoom) {

	// $max_zoom のタイルを切り取って拡大する

	$pw = pow(2,$z - $max_zoom);

	$zz = $max_zoom;
	$xx = floor($x / $pw);
	$yy = floor($y / $pw);

	$dw = 256;
	$sw = floor($dw / $pw);
	$sx = $x % $pw * $sw;
	$sy = $y % $pw * $sw;

	$path = "./layers/tile/{$zz}/{$xx}/{$yy}.png";

	if (file_exists($path)) {

		$src = imagecreatefrompng($path);

		$dst = imagecreatetruecolor($dw,$dw);
		imageAlphaBlending($dst,false);
		imageSaveAlpha($dst,true);
		$transparent = imageColorAllocateAlpha($dst, 0,0,0, 0);
		imageFill($dst, 0,0, $transparent);

		imagecopyresampled($dst, $src, 0,0, $sx,$sy, $dw,$dw, $sw,$sw);

		imagepng($dst);
		exit;

	} else {

		header("HTTP/1.1 404 Not Found");
		exit;
	}

} else {

	// 実ファイルにリダイレクト

	$url = "./layers/tile/{$z}/{$x}/{$y}.png";

	header('Location: '.$url);
	exit;

}
関連ページ

XYZタイルが正しく作成できているか確認するのに便利な、タイルのインデックスを表示するサービスを作ってみました。 単純に、受け渡されたXYZを表示する256×256ドットの画像を作って返すだけというものです。 OpenLayers では、下記のようにタイルレイヤを追加してください。

var ixlayer = new OpenLayers.Layer.XYZ(
	'インデックスレイヤー', 
	'http://landhere.info/services/tile/iximg.php?x=${x}&y=${y}&z=${z}'
);
map.addLayer(ixlayer);

ソースコードも掲載しておきますので、ご利用ください。 描画用フォントが必要なので、適当なものを用意して、$font にパスを入れて下さい。 半透明にしたい場合は、PHP でも、OpenLayers でもお好きな方で対応してください。

<?php

$z = $_REQUEST['z'];
$x = $_REQUEST['x'];
$y = $_REQUEST['y'];

$png = imagecreatetruecolor(256,256);

$white = imagecolorallocate($png, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($png, 0x00, 0x00, 0x00);
$font = 'yourfont.ttf';
$text = "z={$z},x={$x},y={$y}";

$ix = 10;
$iy = 20;

imagefttext($png,10,0,$ix,$iy,$white,$font,$text);

imagerectangle($png,0,0,255,255,$white);

header('Content-type: image/png');
imagepng($png);

?>

レイヤーの重ねあわせ順を制御するのに layer.setZIndex は良い結果を生みません。 そのような場合は、map.setIndex を使用します。 この関数を使用すると、レイヤーのインデックスが振り直されることに注意してください。 例えば、レイヤーを再背面に移動したい場合は、setIndex(Layer, -1) のようにコールしますが、インデックス番号0に Layer が配置され、それ以降のレイヤーは1ずつ後ろへシフトされます。 このため、インデックスは任意の番号を与えることができません。

実装上は、レイヤーリストやレイヤーツリーとして別途管理しておき、レイヤーの追加や削除が行われたときは、すべてのレイヤーについて順番に map.setIndex(Layer, -1) をすると良いです。

レイヤーの順序を変更していたりすると、ポップアップの枠が最上位に表示されなくなることがあります。 調べた CSS のクラス olPopup に、z-index を指定してみましたが、OpenLayers が生成する際に再設定されているらしく、この指定は無視されました。 下記のように再々設定をしてみたところ、希望通りの表示になりました。

map.addPopup(popup);
$(".olPopup").css("z-index", 10000);

OpenLayers のタイルレイヤーで、画像が無いエリアにエラー画像が表示が表示されないようにする方法です。 タイルレイヤーの範囲外の部分や、データが提供されていないズームレベルで、エラー画像で埋め尽くされるのを防ぎ、後ろのレイヤーを透過させることができます。

エラー画像は、olImageLoadError というクラスが付いていますので、下記のように非表示を強制するよう CSS を記述すれば OK です。

.olImageLoadError { 
    display: none !important;
}

OpenLayers の縮尺レベルは、デフォルトで(小縮尺)0~15(大縮尺)です。 この範囲は numZoomLevels プロパティで表され、デフォルトでは16になっています。 例えば、0~20に設定したい場合は、以下のようなコードになります。

	map = new OpenLayers.Map({
		numZoomLevels:     21,
		:
	});

OpenLayers をバージョンアップしたところ、jQuery の draggable を適用したフローティング・パネルの動きが変になってしまいました。ゆっくりと動かして、ドラッグハンドルをマウスが外れなければ正しく動きますが、速く(といっても普通に)動かして、ドラッグハンドルをマウスが少しでも外れると、ドラッグが解除されたり、おかしな動作が始まります。マウスダウンからアップまで、ハンドリングできない状態になり、低スペックマシンではかなり操作しづらくなります。

なかなか原因が分からなくて、旧バージョンにロールバックしようかと思いましたが、分かってしまえば至って単純な原因でした。OpenLayers の新しいバージョンは、fallThrouth というオプションがデフォルトで false になっています。これを true にするだけでした。

	map = new OpenLayers.Map({
		fallThrough:       true
	});

OpenLayers でバッファの発生とかは可能なようですが、ポリゴン同士の幾何演算(融合や差分)などを行う関数が用意されていません。コントロールで Split がありますが、これはマウスで描いたラインを使ってジオメトリを分割するようなインターフェイスです。

色々と調べてみると、JSTS Topology Suite というライブラリを見つけました。Java のライブラリのようですが、JavaScript のものもあり、OpenLayers と連携して使うための仕組みも用意されています。

JSTS Topology Suite

https://github.com/bjornharrtell/jsts

サンプルコード

var reader = new jsts.io.WKTReader();

var a = reader.read('POLYGON((10 10, 100 10, 100 100, 10 100, 10 10))');
var b = reader.read('POLYGON((50 50, 200 50, 200 200, 50 200, 50 50))');

var diff = a.difference(b);

var parser = new jsts.io.OpenLayersParser();

diff = parser.write(diff);

var map = new OpenLayers.Map('map', {
	maxExtent: new OpenLayers.Bounds(0, 0, 300, 300),
	maxResolution: 100,
	units: 'm',
	controls: [new OpenLayers.Control.MousePosition(), new OpenLayers.Control.Navigation()]
});

var layer = new OpenLayers.Layer.Vector('test', {isBaseLayer: true});
map.addLayer(layer); 

var diffOutput = new OpenLayers.Feature.Vector(diff, null, { fillColor: 'green', fillOpacity: 1});
layer.addFeatures([diffOutput]);

layer.addFeatures([diffOutput]);
map.zoomToMaxExtent();

距離や面積を求める getLength や getArea といった関数が用意されていますが、この関数が返す値は球面座標系では意味を成さず、EPSG:900913などの投影座標系では計算誤差が生じて使えません。そのような場合、これらの関数の代わりに getGeodesicLength や getGeodesicArea を使うと良いみたいです。

geom.getGeodesicLength( srcProjection );
geom.getGeodesicArea( srcProjection );

詳しい説明が見つからないのですが、srcProjection で与えた座標系から変換して WGS84 楕円体に沿ったの距離や面積を計算してるんじゃないかと思います。(Geodecic は「測地線」という意味らしい)

マップに配置した PanZoomBar などのコントロールを消したり戻したりするのに、わざわざ removeControl しなくても下記のようなコードでできます。div プロパティが、PanZoomBar 全体の div 要素を保持しているので、その CSS を書き換えることができます。なお、再表示したい場合は、none の部分を block にします。

myPanZoomBar.div.style.display = 'none';