L.GridLayerでコピペ可能なタイル座標の表示

※この投稿は、FOSS4G 二個目だよ Advent Calendar 2016 - Qiitaの12月7日分の記事です。

国土地理院タイル座標確認ツールって便利ですよね。
このタイル座標をタイル上で選択してコピーできたらいいなと思って自分でも作ってみました。

こんな風に書きゃいいのかなと思っていたんですが、そもそも下記tile要素にはclickイベントなんか無いんで、mapのイベントをキャンセルできないんですよね。

var GridLayerClass = L.GridLayer.extend({
    createTile: function(coords){
        var tile = L.DomUtil.create('div', 'leaflet-tile mytile');
        var coordsContainer = L.DomUtil.create('ul', 'mycoords',tile);

        coordsContainer.innerHTML = "<li>こっちは選択できない</li>"
            + "<li>z/x/y:&nbsp;<input type=\"text\" value=\" "+ coords.z + "/" + coords.x + "/"  + coords.y  +  " \" ></li>";

        L.DomEvent.on(coordsContainer,"click",L.DomEvent.stopPropagation);
        L.DomEvent.on(coordsContainer,"mousedown",L.DomEvent.stopPropagation);
        L.DomEvent.on(coordsContainer,"dblclick",L.DomEvent.stopPropagation);
        L.DomEvent.on(coordsContainer,"mousewheel",L.DomEvent.stopPropagation);

        return tile;
    },

});

で、結局こんな風にしました。
https://monomoti.github.io/TileCoordsTile/


ソースはこちら。
TileCoordsTile/app.js at master · monomoti/TileCoordsTile · GitHub

来年もよろしくお願いします。

メリークリスマス&ハッピーニューイヤー!

ストレスの原因を特定してスッキリ

先日、同僚がStrengthFinderの結果を見せてくれた。
自分も過去に診断したことがあったので結果を改めて見直してみた。

「自分の強みTop5」は、戦略性、着想、未来志向、内省、個別化とのこと。

今になって気がついたのは、これれらは「自分の強み」というよりむしろ、「これがない奴にイラつくTop5」ということ。
特に戦略性と個別化。あって当然と思っていることが無いことへの苛立ち。これだー!。
これからは、イライラするたびに「落ち着け、お前の強みだ。」と自分に言い聞かせようと思う。

Leaflet v1.0.0-rc.2で北上でないCRSでmap.fitBounds()が動かない件

北が上にならないCRSで、map.fitBoundsが動かなかった。目的のL.LatLngBoundsのスクリーン上の左上座標と右下座標を、CRSを考慮せずにそれぞれgetNorthWestとgetSouthEastで取得していたためだった。

Map.jsのgetBoundsZoomを下記の様に修正して対応。

修正前

getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
    bounds = L.latLngBounds(bounds);
    padding = L.point(padding || [0, 0]);

    var zoom = this.getZoom() || 0,
        min = this.getMinZoom(),
        max = this.getMaxZoom(),
        nw = bounds.getNorthWest(),
        se = bounds.getSouthEast(),
        size = this.getSize().subtract(padding),
        boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)),
        snap = L.Browser.any3d ? this.options.zoomSnap : 1;

    var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y);
    zoom = this.getScaleZoom(scale, zoom);

    if (snap) {
        zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
        zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
    }

    return Math.max(min, Math.min(max, zoom));
},

修正後

getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
    bounds = L.latLngBounds(bounds);
    padding = L.point(padding || [0, 0]);

    var zoom = this.getZoom() || 0,
        min = this.getMinZoom(),
        max = this.getMaxZoom(),
        nw = bounds.getNorthWest(),
        se = bounds.getSouthEast(),
        size = this.getSize().subtract(padding);

    // pt2 > pt1でないと動かない。
    var pt1 = this.project(se, zoom),
        pt2 = this.project(nw, zoom);        
    if (pt2.y > pt1.y){
        var pt2y = pt2.y;
        pt2.y = pt1.y;
        pt1.y = pt2y;
    }
    var boundsSize = pt1.subtract(pt2),
        snap = L.Browser.any3d ? this.options.zoomSnap : 1;

    var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y);
    zoom = this.getScaleZoom(scale, zoom);

    if (snap) {
        zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
        zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
    }

    return Math.max(min, Math.min(max, zoom));
},

他にもいろいろ、普通の使い方でないところでつまづきそう。

Django Rest Framework GISで誰でも簡単RESTful Geo API

※この投稿は、FOSS4G Advent Calendar 2015 - Qiitaの15日目の記事です。


こんにちは、本格派アフロの非本格派プログラマmonomotiです。久しぶりにFOSS4G Advent Calendarに参加します。

はじめにお断りしておきます。当記事には残念ながら何一つ新しい/オリジナルなネタはございません。
便利なジオ系フレームワークのご紹介です。周りにGeoDjangoを使っている人がいなくて、かなり寂しいので、ユーザを増やしてキャッキャしたいなというだけです。
公式ドキュメントに書いてある事ばかりですが、「あちこち英語のドキュメントを見るのがめんどくさい」という方が使い始める切っ掛けになれば幸いです。

さて、今回ご紹介するのは、PythonのWebアプリケーションフレームワークDjangoの地理空間系モジュールであるGeoDjangoと、Django Rest Framework GISを用いた、RESTfulなWeb APIの作り方です。
作成するAPIは、httpクライアントでjsonフォマットのデータを送受信して、サーバの地理空間データの作成・取得・更新・削除を行うものとします。
また、RESTfulとはなんぞや、という件についてはこちら(https://ja.wikipedia.org/wiki/REST)をご覧下さい。

以下、pyenv + virtualenvでpython3.4、データベースはPostgreSQLPostGISがインストール済みという環境を前提に、ご説明します。


GeoDjangoのセットアップと初期データの準備

では、本題のAPI作成の前に、GeoDjangoのチュートリアルの流れに沿って、さっくりとGeoDjangoアプリケーションの作成から初期データの準備までやってしまいます。

Djangoをインストールします。

$ pip install django
>> Collecting django
>>   Using cached Django-1.8.7-py2.py3-none-any.whl
>> Installing collected packages: django
>> Successfully installed django-1.8.7

データベースを作成します。名前はAdvent Calendar 2015ということでac2015にします。

$ createdb -U postgres -E UTF-8 ac2015
$ psql -U postgres -d ac2015 -c "CREATE EXTENSION postgis;"
>> CREATE EXTENSION

適当なディレクトリを作ってそこに移動し、Djangoのプロジェクトを作成します。プロジェクトの名前は何でもよいですが、ここではチュートリアルと同じgeodjangoにします。

$ mkdir blog20151215
$ cd blog20151215
$ django-admin startproject geodjango

このようなディレクトリ・ファイル群が作成されます。

geodjango/
├── geodjango
│    ├── __init__.py
│    ├── settings.py
│    ├── urls.py
│    └── wsgi.py
└── manage.py

geodjangoに移動します。

$ cd geodjango

Webアプリケーションを作成します。ここも名前は何でもよいですが、ac2015にします。

$ python manage.py startapp ac2015

すると一番上のgeodjangoディレクトリ以下、こういうディレクトリ・ファイル群になります。

geodjango/
├── manage.py
├── geodjango
│    ├── __init__.py
│    ├── settings.py
│    ├── urls.py
│    └── wsgi.py
│           
└── ac2015
      ├── __init__.py
      ├── admin.py
      ├── migrations
      │    └── __init__.py
      ├── models.py
      ├── tests.py
      └── views.py

以降、一番上のgeodjangoディレクトリからの相対パスで説明します。

geodjango/settings.pyを開いて、DATABASESとINSTALLED_APPSを次のように修正します。

# geodjango/settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'ac2015',
        'USER': 'postgres',
        'HOST':'localhost',        
    }
}

#'django.contrib.gis'と'ac2015'を追加。
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.gis',
    'ac2015',
)


初期データをインポートします。
チュートリアルのWorldBorderだとMultiPolygonでAPIの説明がちょっと面倒になるので、ここではこんな感じの自作のPointデータを使用します。僕が「行きたい飲食店リスト」です。
gist.github.com



まずはDBとアプリケーション間のデータを仲介する為の、データモデルをac2015/models.pyに定義します。

#ac2015/models.py

from django.contrib.gis.db import models
class IkitaiOmise(models.Model):
    name = models.CharField(max_length=50)
    category = models.CharField(max_length=50,null=False,blank=True,default="")
    geom = models.PointField(srid=4326)

    def __str__(self):
        return self.name

下記コマンドでマイグレーションファイルを作成します。マイグレーションファイルとは、データモデルの変更内容をデータベースに反映させる為の命令を記述するファイルです。
#実行時、pythonのライブラリ(psycopg2等)が無いと怒られるかもしれません。その場合は適宜pipでインストールして下さい。

$ ./manage.py makemigrations
>> Migrations for 'ac2015':
>>  0001_initial.py:
>>    - Create model IkitaiOmise


下記コマンドで、データモデルの変更内容をデータベースに反映させます。これをマイグレーションと言います。

$ ./manage.py migrate
>> Operations to perform:
>>   Synchronize unmigrated apps: messages, staticfiles, gis
>>   Apply all migrations: sessions, ac2015, contenttypes, admin, auth
>> Synchronizing apps without migrations:
>>   Creating tables...
>>     Running deferred SQL...
>>   Installing custom SQL...
>> Running migrations:
>>   Rendering model states... DONE
>>   Applying ac2015.0001_initial... OK
>>   Applying contenttypes.0001_initial... OK
>>   Applying auth.0001_initial... OK
>>   Applying admin.0001_initial... OK
>>   Applying contenttypes.0002_remove_content_type_name... OK
>>   Applying auth.0002_alter_permission_name_max_length... OK
>>   Applying auth.0003_alter_user_email_max_length... OK
>>   Applying auth.0004_alter_user_username_opts... OK
>>   Applying auth.0005_alter_user_last_login_null... OK
>>   Applying auth.0006_require_contenttypes_0002... OK
>>   Applying sessions.0001_initial... OK

インポートするgeojsonファイル(ikitai_omise.json)を適当な場所配置します。どこでも良いのですが、ここではac2015/dataにしておきます。


ac2015ディレクトリの下に、データのインポートをするスクリプトファイルを作成します。ここではチュートリアルと同じく、load.pyという名前にします。

#load.py

import os
from django.contrib.gis.utils import LayerMapping
from ac2015.models import IkitaiOmise

ikitai_omise_mapping = {
    'name' : 'name',
    'category' : 'category',
    'geom' : 'POINT',
}

ikitai_omise_geojson = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data', 'ikitai_omise.json'))

def run(verbose=True):
    lm = LayerMapping(IkitaiOmise, ikitai_omise_geojson, ikitai_omise_mapping,
                      transform=False, encoding='UTF-8')

    lm.save(strict=True, verbose=verbose)


djangoのshellで、上で作成したスクリプトを実行します。これでikitai_omise.jsonの内容がデータベースにインポートされます。

$ ./manage.py shell
>>> from ac2015 import load
>>> load.run()
>>> ...
>>> Saved: インドラディップ INDRADIP
>>> Saved: Buci boccheno
>>> Saved: Pie and Beer PUB ハバ
>>> Saved: トタンにライスカレー
>>> Saved: いちにいさん
>>> Saved: らーめん処さんさん
>>> Saved: (株)ダンディー 二号店
>>> Saved: ベビーフェイスプラネッツ 橿原店
>>> Saved: 葛城ガーデン
>>> Saved: アローム
>>> Saved: 奈良県葛城市當麻 中将堂本舗
>>> Saved: エアーズカフェ岡谷店
>>> Saved: 麺屋 誠和
>>> Saved: Barm's
>>> Saved: 浜勝
>>> Saved: お好み鉄板がるぼ
>>> Saved: 二刀流

このdjango.contrib.gis.utils.LayerMapping、超便利です。もう少し突っ込んだ使い方については、また別の機会にご紹介したいと思います。

シェルを終了します。

>>> exit()
$

Djangoのとても便利な点の一つは、最初からデータ管理画面が用意されているので、何もプログラミングしなくてもデータの入力・編集を行える事です。
先ほどロードしたデータをこのデータ管理画面で確認してみましょう。

ac2015/admin.pyを下記の通りに修正します。

#ac2015/admin.py

from django.contrib.gis import admin
from ac2015.models import IkitaiOmise

admin.site.register(IkitaiOmise, admin.GeoModelAdmin)

geodjango/urls.pyの

from django.contrib import admin

from django.contrib.gis import admin

に修正します。

djangoアプリケーションの管理者権限を持つユーザを作成します。

$ ./manage.py createsuperuser
>> Username (leave blank to use 'myname'): myname
>> Email address: mymail@mymail.com
>> Password: 
>> Password (again): 
>> Superuser created successfully.

djangoアプリケーションを起動します。

$ ./manage.py runserver
>> December 15, 2015 - 07:09:57
>> Django version 1.8.7, using settings 'geodjango.settings'
>> Starting development server at http://127.0.0.1:8000/
>> Quit the server with CONTROL-C.

Webブラウザで http://127.0.0.1:8000/adminを開き、先ほど作成したユーザ名とパスワードでログインします。

「Ac2015」の 「Ikitai omises」をクリックすると、ロードしたデータのリストが表示されます。

リスト中のデータのリンクをクリックすると、詳細が表示されます。

geomフィールドのデータがOpenLayersで表示されています。

djangoアプリを終了する時は、コンソールでCONTROL-Cを入力します。

超簡単!RESTful APIの作成

さて、やっと本題です。RESTfulなジオAPIを作成します!

Django Rest Frameworkのジオ拡張であるdjango-rest-framework-gisと、検索機能で使うdjango-filterをインストールします。
また、APIの実行結果をブラウザで表示する為に、markdownもインストールします。

$ pip install djangorestframework-gis
$ pip install django-filter
$ pip install markdown

ac2015/setting.pyのINSTALLED_APPAに、'rest_framework'と'rest_framework_gis'を追加します。

#ac2015/setting.py

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.gis',
    'ac2015',
    'rest_framework',
    'rest_framework_gis',
)

リアライザを作成します。シリアライザとは、データベースのレコードとAPIで送受信するデータフォーマットの(ここではjson)の仲介役の事です。シリアライザは、APIでデータを呼び出す時はレコードの内容をjsonにして返してくれます。APIjsonを送信すると、jsonの内容で既存のレコードが更新されたり、新しいレコードが作られたりします。
ac2015ディレクトリの下に、下記内容のserializers.pyを作成します。

#ac2015/serializers.py

from rest_framework.serializers import ModelSerializer
from ac2015.models import IkitaiOmise

class IkitaiOmiseSerializer(ModelSerializer):
	class Meta:
		model = IkitaiOmise

ここで作成したのは、rest_frameworkが提供するModelSerializerのサブクラスです。ac2015/models.pyで定義したデータモデル(IkitaiOmise)とこのシリアライザを関連づけています。やっていることはこれだけです。

次に、REST Frameworkのビューを作成します。ここでいうビューとは、httpリクエストに対してのどう振る舞って、どういうレスポンスを返すのかを定義するもののことを言います。
もう少し具体的に言うと、「データを更新するのか、作成するのか、削除するのか、1取得するのか、複数取得するのか、複数取得するならどういう絞り込みをするのか」という事を定義します。

ここでは、複数件を取得する時は、任意の緯度経度から任意の距離内にあるお店だけを取得するビューを定義してみます。
上のほうでac2015アプリケーションを作成したときに、ac2015/view.pyというファイルが作成されているので、これを下記のように編集します。

# ac2015/view.py

from rest_framework_gis.filters import DistanceToPointFilter
from rest_framework import generics,viewsets
from ac2015 import models,serializers
from rest_framework.pagination import PageNumberPagination


class MyPagination(PageNumberPagination):
    page_size_query_param = 'page_size'

class IkitaiOmiseViewSet(viewsets.ModelViewSet):
    queryset = models.IkitaiOmise.objects.all()
    serializer_class = serializers.IkitaiOmiseSerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter,)
    distance_filter_field = 'geom'
    distance_filter_convert_meters = True

ここでやっているのことを大まかに説明すると、以下の通りです。

  • serializer_classで、どのシリアライザを使うかを設定。
  • pagination_classで、どういうページングをするかを設定。ここではpage_sizeでURLクエリパラメータでページングのサイズを指定出来るPageNumberPaginationクラスのサブクラス(MyPagination)を使っています。
  • filter_backendsで、データを絞り込む方法を設定。ここでは、指定した点からの距離で絞り込むDistanceToPointというフィルタを使っています。
  • distance_filter_fieldで、データモデルのどのフィールドを絞り込みの対象とするかを設定しています。


次に上で作成したビューを、urlと対応付けます。
まず、geodjango/urls.pyを次のように修正します。

#geodjango/urls.py

from django.conf.urls import include, url
from django.contrib.gis import admin

urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^', include('ac2015.urls',namespace='ac2015')),
]


つぎに下記内容のac2015/urls.pyを作成します。

from django.conf.urls import patterns, include, url
from rest_framework import routers
from ac2015 import views

router = routers.DefaultRouter()
router.register(r'places', views.IkitaiOmiseViewSet)

urlpatterns = patterns('',

	url(r'^api/', include(router.urls)),
)

これでビューとurlの関連付けが出来ました。
そしてこれで、地理空間データの作成・取得・更新・削除を行うRESTfulなAPIが出来ました。
はい、これだけです。超簡単!

さっそくAPIを試してみましょう。
djangoアプリケーションを起動します。

$ ./manage.py runserver


それでは、近年話題のウラなんば、緯度34.66542、経度135.50324の半径300m内にあるお店を検索してみましょう。

Django Rest API Frameworkは、APIをwebブラウザでテストする為のUIが用意されているので、いちいちコンソールから

$ curl -X GET -H "ContentType:application/json" http://localhost:8000/api/places/?dist=300&point=135.50324,34.66542 

なんてタイプする必要がありません。Webブラウザで、下記のURLを開いてみて下さい。

http://localhost:8000/api/places/?dist=300&point=135.50324,34.66542


お店が出てきました。超簡単でしょう?
全くプログラミングはしていないのに、ちょっとビューを設定するだけでここまで出来てしまいます。
ごらんのとおり、位置情報であるgeomカラムのデータは、GeoJSONのgeometry属性のフォーマットで表現されています。

{
        "id": 67,
        "name": "かき小屋フィーバー ボーボーアフロ",
        "category": "レストラン",
        "geom": {
            "type": "Point",
            "coordinates": [
                135.505652,
                34.664452
            ]
        }
    }


「仕様でPointだと分かっているのに"type": "Point"は冗長だ」とか「Leafletで扱いやすい[lat,lng]の方がいい」という場合は、シリアライザのto_representation(シリアライズ側)とto_internal_value(デシリアライズ側)というメソッドに手を加えれば、

    {
        "id": 67,
        "name": "牡蠣小屋フィーバー ボーボーアフロ",
        "category": "レストラン",
        "geom": [
            34.664452,
            135.505652
        ]
    },

という様にすることができます。ですが今回は「プログラミング無し!」にしたいので、このままにします。

検索によってデータのIDが分かったので、任意の1件取得してみましょう。

http://localhost:8000/api/places/{id}/ の書式でIDを指定します。

例えば、id=67なら、下記URLになります。

http://localhost:8000/api/places/67/

あれ?お店の名前が間違っている事に気づきました。大変失礼致しました。「牡蠣小屋」ではなく「かき小屋」ですので修正します。

データの更新はPUTメソッドです。PUTボタンクリックすると...

修正されました。

次はデータの追加を試してみましょう。
谷町九丁目駅から1km内で検索してみると(http://localhost:8000/api/places/?dist=1000&point=135.5157,34.66647)...

お店が1件しか無いのは寂しいので、追加します。ブラウザからでも出来ますが、Web APIであることを実感するために、こんどはコンソールからやってみましょう。
次のコマンドをコピー&ペーストして実行してみて下さい。

curl -X POST  -H "Accept: application/json" -H "Content-type: application/json" -d '{"name": "旧ヤム邸宅","category": "レストラン","geom":{"type": "Point","coordinates": [135.5129899,34.6736539]}}' http://localhost:8000/api/places/

下のような応答があれば成功です。

>> {"id":109,"name":"旧ヤム邸宅","category":"レストラン","geom":{"type":"Point","coordinates":[135.5129899,34.6736539]}}

先ほどの検索結果をリロードしてみましょう。

新しいデータがid=109で作成されていますね。

因に、検索の際、結果の件数が多かったのでページングはしていていませんでしたが、件数が多い場合はページングするのが普通です。
ページングは、URLクエリパラメータにpage_size={1ページ当たりの件数}&page={ページ番号}を追加する事で行います。
例えば、大阪梅田から1km内のお店を10件ずつ取得したい場合は、URLは

http://localhost:8000/api/places/?dist=1000&point=135.4953,34.7022&page_size=10&page=1
http://localhost:8000/api/places/?dist=1000&point=135.4953,34.7022&page_size=10&page=2

のようになります。

さて、ここまで任意の点からの距離で検索してきましたが、他にもいろいろなフィルタリング方法が用意されています。
最後にバウンダリでのフィルタリングをご紹介します。

ac2015/views.pyを下記のように編集します。

# ac2015/views.py

from rest_framework_gis.filters import DistanceToPointFilter, InBBoxFilter # <=ここを変更
from rest_framework import generics,viewsets
from ac2015 import models,serializers
from rest_framework.pagination import PageNumberPagination


class MyPagination(PageNumberPagination):
    page_size_query_param = 'page_size'

class IkitaiOmiseViewSet(viewsets.ModelViewSet):
    queryset = models.IkitaiOmise.objects.all()
    serializer_class = serializers.IkitaiOmiseSerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter, InBBoxFilter)  # <=ここを変更
    distance_filter_field = bbox_filter_field = 'geom' #  <=ここを変更
    distance_filter_convert_meters = True

変更の内容は、以下のとおりです。

  • filter_backendsにInBBoxFilterを追加して、バウンダリでの絞り込みも出来る様に設定。
  • bbox_filter_fieldで、絞り込み対象のフィールドを設定。

では、経度135.48,緯度34.6969を南西端、経度135.4984,緯度34.7055を北東端とするバウンダリで検索してみましょう。
URLは下記の様に指定します。

http://localhost:8000/api/places/?in_bbox=135.48,34.6969,135.4984,34.7055

すごい!簡単ですね!

こんな感じで、プログラミングなしでも、GeoJSONとDjango REST Frameworkで色々出来てしまいます。
皆さん是非使ってみて下さい!

明日はZero_Kohakuさんです!


追記:
ちなみに、Django Rest Framework GISには完全なGeoJSONを扱うシリアライザがあり、TMSTileFilterというフィルタもあるので、「これでGeoJSONタイルサービスができるのかな?」と思ったのですが、ソースを見た所TMSTileFilterはその名のとおり絞り込みを行うだけで、タイル上にベクターをカットしてくれる訳ではありませんでした。
GeoJSONタイルサービスを作りたいなら、下記で紹介されているdjgeojsonというモジュールを使うのがよさそうです(1年も前の記事なのか...汗)。

PostGISとGeoDjangoを使ってLeafletでGeoJSON Tile Layerを表示してみる(5) – GeoDjangoでTiled GeoJSONを出力する – – ビットログ

LeafletのIControlについて

この投稿は、FOSS4G 二個目だよ Advent Calendar 2015 - Qiita の8日目の記事です。
monomotiです。
8日目が埋まってないけど(*1)、諦めないで!ってことで極小ネタを。

Leaflet v0.7(*2)で、例えば、クリックした点の座標をLeafeltのIControlに表示しようと、こういうものを作ったとして、

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>

  <script src='https://api.mapbox.com/mapbox.js/v2.2.3/mapbox.js'></script>
  <link href='https://api.mapbox.com/mapbox.js/v2.2.3/mapbox.css' rel='stylesheet' />
  <style type="text/css">
  html,body,#map-container, #map{
    margin:0;
    padding:0;
    width:100%;
    height:100%;
    cursor: pointer;
  }
  #map-container .center-icon{
    font-size:40px;
    color:#f00;
    position:absolute;top:50%;left:50%;margin-top:-20px;margin-left:-20px;
    z-index:9999;
  }
  .info-container{
    background-color:#ccc;
    width:200px;
    height:70px;
    padding:10px;
    overflow: auto;
  }
    
  </style>
</head>
<body>

  <div id="map-container">
    <div id="map"></div>
    <div class="center-icon">+</div>
  </div>

  <script type="text/javascript">
    L.mapbox.accessToken = "とーくん";
    var map = L.mapbox.map('map', 'mapbox.streets');

    var InfoControl = L.Control.extend({
      options:{position:"topright"},
      onAdd:function(){
        container = L.DomUtil.create('div',"info-container");
        container.innerHTML = "POSITION:<div id=\"position\">";

        return container;
      }
    });

    var info_control = new InfoControl();
    map.addControl(info_control);
    var posDiv = document.getElementById("position");
    map.on("click",function(e){
      posDiv.innerText = e.latlng.toString();
    },this);

  </script>
</body>
</html>

f:id:monomoti:20151208152841j:plain

Controlに表示された座標をコピーしようとすると、
f:id:monomoti:20151208152959j:plain
地図のクリックイベントが発火して座標が変わっちゃうし、ドラッグもされちゃう!

こんなときは、こうします。

    var InfoControl = L.Control.extend({
      options:{position:"topright"},
      onAdd:function(){
        container = L.DomUtil.create('div',"info-container");
        container.innerHTML = "POSITION:<div id=\"position\">";

        L.DomEvent.on(container,"click",L.DomEvent.stopPropagation);    //ここ
        L.DomEvent.on(container,"mousedown",L.DomEvent.stopPropagation);    //ここ
        L.DomEvent.on(container,"dblclick",L.DomEvent.stopPropagation);    //ここ
        L.DomEvent.on(container,"mousewheel",L.DomEvent.stopPropagation);    //ここ

        return container;
      }
    });

どうでもいいネタですみません!

*1:全くの杞憂でした...。暫くして凄い方々が次々と記事を投稿されていますね。恥ずかしい...。

*2:mapbox.jsを使っていますが

FOSS4G2013、今年も開催します!懇親会参加者、大募集アンド大募集アンド大募集

10/31〜11/2に東京、11/6,7に大阪で、フリー&オープンなGISの祭典、FOSS4G?が開催されます。

詳細はOSGeo.jp公式サイト(http://www.osgeo.jp/イベント/foss4g2013tokyo/http://www.osgeo.jp/イベント/foss4g2013osaka/)をご覧ください。
GISとはGeographic Information Systemの略で「地理的位置を手がかりに、位置に関する情報を持ったデータ(空間データ)を総合的に管理・加工し、視覚的に表示し、高度な分析や迅速な判断を可能にする技術」(「GISとは・・・|国土地理院」より)のことです。

また、懇親会も開催します。参加者大募集中です。(特に大阪!)

FOSS4G 2013 Tokyo 大懇親会 11/1(金) : ATND
FOSS4G2013 OSAKA 懇親会 & Tシャツ | Peatix

関西オリジナル版をつくりました!(僕じゃないです)
http://d.hatena.ne.jp/monomoti/files/TShirt_2013_OSKAKA.png?d=.png

FOSS4G2013、今年も開催します!懇親会参加者、大募集アンド大募集アンド大募集

10/31〜11/2に東京、11/6,7に大阪で、フリー&オープンなGISの祭典、FOSS4G?が開催されます。

詳細はOSGeo.jp公式サイト(http://www.osgeo.jp/イベント/foss4g2013tokyo/http://www.osgeo.jp/イベント/foss4g2013osaka/)をご覧ください。
GISとはGeographic Information Systemの略で「地理的位置を手がかりに、位置に関する情報を持ったデータ(空間データ)を総合的に管理・加工し、視覚的に表示し、高度な分析や迅速な判断を可能にする技術」(「GISとは・・・|国土地理院」より)のことです。

また、懇親会も開催します。参加者大募集中です。(特に大阪!)

FOSS4G 2013 Tokyo 大懇親会 11/1(金) : ATND
FOSS4G2013 OSAKA 懇親会 & Tシャツ | Peatix