Commit ec0f1054 authored by Serge S. Koval's avatar Serge S. Koval

Merge pull request #708 from singingwolfboy/geoa

Added geoa contrib module for Geoalchemy2 integration
parents b778848a 6232246b
......@@ -5,11 +5,20 @@ python:
- "3.3"
env:
- REQUIREMENTS=travis_wtf1.txt
- REQUIREMENTS=travis_wtf2.txt
- WTFORMS_VERSION=1
- WTFORMS_VERSION=2
install: "pip install -r $REQUIREMENTS --use-mirrors"
services:
- postgresql
- mongodb
before_script:
- psql -U postgres -c 'CREATE DATABASE flask_admin_test;'
- psql -U postgres -c "CREATE EXTENSION postgis;" flask_admin_test
install:
- pip install "wtforms<$WTFORMS_VERSION.99"
- pip install -r dev-requirements.txt
services: mongodb
script: nosetests flask_admin/tests
# Translations template for Flask-Admin.
# Copyright (C) 2012 ORGANIZATION
# Copyright (C) 2014 ORGANIZATION
# This file is distributed under the same license as the Flask-Admin
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2012.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Flask-Admin VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2012-11-09 03:54+0200\n"
"POT-Creation-Date: 2014-11-24 03:04-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"
"Generated-By: Babel 1.3\n"
#: ../flask_admin/base.py:283
#: ../flask_admin/base.py:399
msgid "Home"
msgstr ""
#: ../flask_admin/form.py:83
msgid "Invalid time format"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:33
#: ../flask_admin/contrib/fileadmin.py:34
msgid "Invalid directory name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:41
#: ../flask_admin/contrib/fileadmin.py:42
msgid "File to upload"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:50
#: ../flask_admin/contrib/fileadmin.py:51
msgid "File required."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:55
#: ../flask_admin/contrib/fileadmin.py:56
msgid "Invalid file type."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:365
msgid "File uploading is disabled."
#: ../flask_admin/contrib/fileadmin.py:60
msgid "Content"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:374
#: ../flask_admin/contrib/fileadmin.py:427
#, python-format
msgid "File \"%(name)s\" already exists."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:381
#: ../flask_admin/contrib/fileadmin.py:446
#: ../flask_admin/contrib/fileadmin.py:512
#: ../flask_admin/contrib/fileadmin.py:565
#: ../flask_admin/contrib/fileadmin.py:602
#: ../flask_admin/contrib/fileadmin.py:645
#: ../flask_admin/contrib/fileadmin.py:693
msgid "Permission denied."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:508
msgid "File uploading is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:521
#, python-format
msgid "Failed to save file: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:400
#: ../flask_admin/contrib/fileadmin.py:561
msgid "Directory creation is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:410
#: ../flask_admin/contrib/fileadmin.py:576
#, python-format
msgid "Failed to create directory: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:432
#: ../flask_admin/contrib/fileadmin.py:598
msgid "Deletion is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:437
#: ../flask_admin/contrib/fileadmin.py:607
msgid "Directory deletion is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:442
#: ../flask_admin/contrib/fileadmin.py:613
#, python-format
msgid "Directory \"%s\" was successfully deleted."
msgid "Directory \"%(path)s\" was successfully deleted."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:444
#: ../flask_admin/contrib/fileadmin.py:615
#, python-format
msgid "Failed to delete directory: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:448
#: ../flask_admin/contrib/fileadmin.py:511
#: ../flask_admin/contrib/fileadmin.py:620
#: ../flask_admin/contrib/fileadmin.py:759
#, python-format
msgid "File \"%(name)s\" was successfully deleted."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:450
#: ../flask_admin/contrib/fileadmin.py:513
#: ../flask_admin/contrib/fileadmin.py:622
#: ../flask_admin/contrib/fileadmin.py:761
#, python-format
msgid "Failed to delete file: %(name)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:469
#: ../flask_admin/contrib/fileadmin.py:641
msgid "Renaming is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:473
#: ../flask_admin/contrib/fileadmin.py:649
msgid "Path does not exist."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:484
#: ../flask_admin/contrib/fileadmin.py:661
#, python-format
msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\""
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:487
#: ../flask_admin/contrib/fileadmin.py:664
#, python-format
msgid "Failed to rename: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:503
#: ../flask_admin/contrib/peewee/view.py:355
#: ../flask_admin/contrib/sqlamodel/view.py:680
#: ../flask_admin/contrib/fileadmin.py:709
#, python-format
msgid "Error saving changes to %(name)s."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:713
#, python-format
msgid "Changes to %(name)s saved successfully."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:720
#, python-format
msgid "Error reading %(name)s."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:723
#: ../flask_admin/contrib/fileadmin.py:732
#, python-format
msgid "Unexpected error while reading from %(name)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:729
#, python-format
msgid "Cannot edit %(name)s."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:746
#: ../flask_admin/contrib/mongoengine/view.py:599
#: ../flask_admin/contrib/peewee/view.py:406
#: ../flask_admin/contrib/pymongo/view.py:343
#: ../flask_admin/contrib/sqla/view.py:924
msgid "Delete"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:504
#: ../flask_admin/contrib/fileadmin.py:747
msgid "Are you sure you want to delete these files?"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:750
msgid "File deletion is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:763
msgid "Edit"
msgstr ""
#: ../flask_admin/contrib/rediscli.py:125
msgid "Cli: Invalid command."
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:36
#: ../flask_admin/contrib/peewee/filters.py:35
#: ../flask_admin/contrib/sqlamodel/filters.py:35
#: ../flask_admin/contrib/pymongo/filters.py:38
#: ../flask_admin/contrib/sqla/filters.py:39
msgid "equals"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:45
#: ../flask_admin/contrib/peewee/filters.py:43
#: ../flask_admin/contrib/sqlamodel/filters.py:43
#: ../flask_admin/contrib/pymongo/filters.py:47
#: ../flask_admin/contrib/sqla/filters.py:47
msgid "not equal"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:55
#: ../flask_admin/contrib/peewee/filters.py:52
#: ../flask_admin/contrib/sqlamodel/filters.py:52
#: ../flask_admin/contrib/pymongo/filters.py:57
#: ../flask_admin/contrib/sqla/filters.py:56
msgid "contains"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:65
#: ../flask_admin/contrib/peewee/filters.py:61
#: ../flask_admin/contrib/sqlamodel/filters.py:61
#: ../flask_admin/contrib/pymongo/filters.py:67
#: ../flask_admin/contrib/sqla/filters.py:65
msgid "not contains"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:74
#: ../flask_admin/contrib/peewee/filters.py:69
#: ../flask_admin/contrib/sqlamodel/filters.py:69
#: ../flask_admin/contrib/pymongo/filters.py:80
#: ../flask_admin/contrib/sqla/filters.py:73
msgid "greater than"
msgstr ""
#: ../flask_admin/contrib/mongoengine/filters.py:83
#: ../flask_admin/contrib/peewee/filters.py:77
#: ../flask_admin/contrib/sqlamodel/filters.py:77
#: ../flask_admin/contrib/pymongo/filters.py:93
#: ../flask_admin/contrib/sqla/filters.py:81
msgid "smaller than"
msgstr ""
#: ../flask_admin/contrib/peewee/view.py:317
#: ../flask_admin/contrib/sqlamodel/view.py:627
#: ../flask_admin/contrib/mongoengine/view.py:493
#, python-format
msgid "Failed to get model. %(error)s"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:512
#: ../flask_admin/contrib/peewee/view.py:357
#: ../flask_admin/contrib/pymongo/view.py:278
#: ../flask_admin/contrib/sqla/view.py:856
#, python-format
msgid "Failed to create record. %(error)s"
msgstr ""
#: ../flask_admin/contrib/peewee/view.py:332
#: ../flask_admin/contrib/sqlamodel/view.py:647
#: ../flask_admin/contrib/mongoengine/view.py:538
#: ../flask_admin/contrib/peewee/view.py:376
#: ../flask_admin/contrib/pymongo/view.py:303
#: ../flask_admin/contrib/sqla/view.py:882
#, python-format
msgid "Failed to update record. %(error)s"
msgstr ""
#: ../flask_admin/contrib/peewee/view.py:342
#: ../flask_admin/contrib/sqlamodel/view.py:666
#: ../flask_admin/contrib/mongoengine/view.py:562
#: ../flask_admin/contrib/peewee/view.py:392
#: ../flask_admin/contrib/pymongo/view.py:329
#: ../flask_admin/contrib/sqla/view.py:908
#, python-format
msgid "Failed to delete record. %(error)s"
msgstr ""
#: ../flask_admin/contrib/peewee/view.py:356
#: ../flask_admin/contrib/sqlamodel/view.py:681
#: ../flask_admin/contrib/mongoengine/view.py:600
#: ../flask_admin/contrib/peewee/view.py:407
#: ../flask_admin/contrib/pymongo/view.py:344
#: ../flask_admin/contrib/sqla/view.py:925
msgid "Are you sure you want to delete selected records?"
msgstr ""
#: ../flask_admin/contrib/peewee/view.py:372
#: ../flask_admin/contrib/sqlamodel/view.py:699
#: ../flask_admin/contrib/mongoengine/view.py:609
#: ../flask_admin/contrib/peewee/view.py:423
#: ../flask_admin/contrib/pymongo/view.py:354
#: ../flask_admin/contrib/sqla/view.py:941
#, python-format
msgid "Record was successfully deleted."
msgid_plural "%(count)s records were successfully deleted."
msgstr[0] ""
msgstr[1] ""
#: ../flask_admin/contrib/peewee/view.py:377
#: ../flask_admin/contrib/sqlamodel/view.py:704
#: ../flask_admin/contrib/mongoengine/view.py:615
#: ../flask_admin/contrib/peewee/view.py:429
#: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:949
#, python-format
msgid "Failed to delete records. %(error)s"
msgstr ""
#: ../flask_admin/contrib/sqlamodel/fields.py:125
#: ../flask_admin/contrib/sqlamodel/fields.py:175
#: ../flask_admin/contrib/sqlamodel/fields.py:180
#: ../flask_admin/contrib/sqla/fields.py:123
#: ../flask_admin/contrib/sqla/fields.py:173
#: ../flask_admin/contrib/sqla/fields.py:178 ../flask_admin/model/fields.py:167
#: ../flask_admin/model/fields.py:216
msgid "Not a valid choice"
msgstr ""
#: ../flask_admin/contrib/sqlamodel/validators.py:33
#: ../flask_admin/contrib/sqla/filters.py:92
msgid "empty"
msgstr ""
#: ../flask_admin/contrib/sqla/filters.py:129
#: ../flask_admin/contrib/sqla/filters.py:179
#: ../flask_admin/contrib/sqla/filters.py:231
msgid "between"
msgstr ""
#: ../flask_admin/contrib/sqla/filters.py:151
#: ../flask_admin/contrib/sqla/filters.py:198
#: ../flask_admin/contrib/sqla/filters.py:252
msgid "not between"
msgstr ""
#: ../flask_admin/contrib/sqla/validators.py:42
msgid "Already exists."
msgstr ""
#: ../flask_admin/model/base.py:869
#: ../flask_admin/contrib/sqla/validators.py:60
#, python-format
msgid "At least %d item is required"
msgid_plural "At least %d items are required"
msgstr[0] ""
msgstr[1] ""
#: ../flask_admin/contrib/sqla/view.py:835
#, python-format
msgid "Integrity error. %(message)s"
msgstr ""
#: ../flask_admin/form/fields.py:89
msgid "Invalid time format"
msgstr ""
#: ../flask_admin/form/fields.py:133
msgid "Invalid Choice: could not coerce"
msgstr ""
#: ../flask_admin/form/upload.py:187
msgid "Invalid file extension"
msgstr ""
#: ../flask_admin/model/base.py:1094
msgid "There are no items in the table."
msgstr ""
#: ../flask_admin/model/base.py:1118
#, python-format
msgid "Invalid Filter Value: %(value)s"
msgstr ""
#: ../flask_admin/model/base.py:1326
msgid "Record was successfully created."
msgstr ""
#: ../flask_admin/model/filters.py:82
#: ../flask_admin/model/base.py:1363
msgid "Record was successfully saved."
msgstr ""
#: ../flask_admin/model/filters.py:99
msgid "Yes"
msgstr ""
#: ../flask_admin/model/filters.py:83
#: ../flask_admin/model/filters.py:100
msgid "No"
msgstr ""
#: ../flask_admin/templates/admin/actions.html:3
#: ../flask_admin/templates/bootstrap2/admin/actions.html:4
#: ../flask_admin/templates/bootstrap3/admin/actions.html:4
msgid "With selected"
msgstr ""
#: ../flask_admin/templates/admin/lib.html:117
#: ../flask_admin/templates/bootstrap2/admin/lib.html:156
#: ../flask_admin/templates/bootstrap3/admin/lib.html:149
msgid "Submit"
msgstr ""
#: ../flask_admin/templates/admin/lib.html:122
#: ../flask_admin/templates/bootstrap2/admin/lib.html:161
#: ../flask_admin/templates/bootstrap3/admin/lib.html:154
msgid "Cancel"
msgstr ""
#: ../flask_admin/templates/admin/file/list.html:8
#: ../flask_admin/templates/bootstrap2/admin/file/edit.html:5
#: ../flask_admin/templates/bootstrap3/admin/file/edit.html:5
#, python-format
msgid "You are editing %(path)s"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:9
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:9
msgid "Root"
msgstr ""
#: ../flask_admin/templates/admin/file/list.html:55
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:62
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:62
#, python-format
msgid "Are you sure you want to delete \\'%(name)s\\' recursively?"
msgstr ""
#: ../flask_admin/templates/admin/file/list.html:63
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:70
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:70
#, python-format
msgid "Are you sure you want to delete \\'%(name)s\\'?"
msgstr ""
#: ../flask_admin/templates/admin/file/list.html:90
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:105
msgid "Upload File"
msgstr ""
#: ../flask_admin/templates/admin/file/list.html:95
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:110
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:110
msgid "Create Directory"
msgstr ""
#: ../flask_admin/templates/admin/file/list.html:109
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:127
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:127
msgid "Please select at least one file."
msgstr ""
#: ../flask_admin/templates/admin/file/rename.html:5
#: ../flask_admin/templates/bootstrap2/admin/file/rename.html:5
#: ../flask_admin/templates/bootstrap3/admin/file/rename.html:5
#, python-format
msgid "Please provide new name for %(name)s"
msgstr ""
#: ../flask_admin/templates/admin/model/create.html:12
#: ../flask_admin/templates/admin/model/list.html:13
#: ../flask_admin/templates/bootstrap2/admin/model/create.html:5
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:5
msgid "Save and Add"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/create.html:16
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:16
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:17
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:16
msgid "List"
msgstr ""
#: ../flask_admin/templates/admin/model/create.html:15
#: ../flask_admin/templates/admin/model/list.html:17
#: ../flask_admin/templates/bootstrap2/admin/model/create.html:19
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/create.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:20
msgid "Create"
msgstr ""
#: ../flask_admin/templates/admin/model/create.html:20
msgid "Save and Add"
#: ../flask_admin/templates/bootstrap2/admin/model/edit.html:5
#: ../flask_admin/templates/bootstrap3/admin/model/edit.html:5
msgid "Save and Continue"
msgstr ""
#: ../flask_admin/templates/admin/model/inline_form_list.html:24
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:10
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:10
msgid "Delete?"
msgstr ""
#: ../flask_admin/templates/admin/model/inline_form_list.html:32
#: ../flask_admin/templates/bootstrap2/admin/model/inline_list_base.html:33
#: ../flask_admin/templates/bootstrap3/admin/model/inline_list_base.html:33
msgid "Add"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:24
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:3
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:3
msgid "Add Filter"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:51
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:17
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:17
msgid "Apply"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:19
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:19
msgid "Reset Filters"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap2/admin/model/layout.html:45
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:38
#: ../flask_admin/templates/bootstrap3/admin/model/layout.html:45
msgid "Search"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:64
msgid "Apply"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:20
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:20
msgid "Create new record"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:66
msgid "Reset Filters"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:56
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:56
msgid "Select all records"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:67
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:76
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:67
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:76
#, python-format
msgid "Sort by %(name)s"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:98
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:98
msgid "Select record"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:74
msgid "Remove Filter"
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:105
msgid "Edit record"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:149
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:114
msgid "Are you sure you want to delete this record?"
msgstr ""
#: ../flask_admin/templates/admin/model/list.html:173
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
msgid "Delete record"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:150
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:150
msgid "Please select at least one record."
msgstr ""
#!/bin/sh
pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-Admin ../flask_admin
pybabel compile -D admin -d ../flask_admin/translations/
Changelog
=========
1.1 (dev)
---------
Highlights:
* Added the ``geoa`` contrib module, for working with `geoalchemy2`_
.. _geoalchemy2: http://geoalchemy-2.readthedocs.org/
1.0.8
-----
......
......@@ -2,27 +2,34 @@ Database backends
=================
The purpose of Flask-Admin is to help you manage your data. For this, it needs some database backend in order to be
able to access that data in the first place. At present, there are four different backends for you to choose
able to access that data in the first place. At present, there are five different backends for you to choose
from, depending on which database you would like to use for your application.
.. toctree::
:maxdepth: 2
db_sqla
db_geoa
db_mongoengine
db_peewee
db_pymongo
If you don't know where to start, but you're familiar with relational databases, then you should probably look at using
`SQLAlchemy <http://www.sqlalchemy.org/>`_. It is a full-featured toolkit, with support for SQLite, PostgreSQL, MySQL,
`SQLAlchemy`_. It is a full-featured toolkit, with support for SQLite, PostgreSQL, MySQL,
Oracle and MS-SQL amongst others. It really comes into its own once you have lots of data, and a fair amount of
relations between your data models.
relations between your data models. If you want to track spatial data like latitude/longitude
points, you should look into `GeoAlchemy`_, as well.
If you're looking for something simpler, or your data models are reasonably self-contained, then
`MongoEngine <http://mongoengine.org/>`_ could be a better option. It is a python wrapper around the popular
*NoSQL* database called `MongoDB <http://www.mongodb.org/>`_.
`MongoEngine`_ could be a better option. It is a python wrapper around the popular
*NoSQL* database called `MongoDB`_.
Of course, if you feel that there's an awesome database wrapper that is missing from the list above, we'd greatly
appreciate it if you could write the plugin for it and submit it as a pull request. A special section of these docs
are dedicated to helping you through this process. See :doc:`model_guidelines`.
.. _SQLAlchemy: http://www.sqlalchemy.org/
.. _GeoAlchemy: http://geoalchemy-2.readthedocs.org/
.. _MongoEngine: http://mongoengine.org/
.. _MongoDB: http://www.mongodb.org/
GeoAlchemy backend
==================
If you want to store spatial information in a GIS database, Flask-Admin has
you covered. The `GeoAlchemy`_ backend extends the SQLAlchemy backend (just as
GeoAlchemy extends SQLAlchemy) to give you a pretty and functional map-based
editor for your admin pages.
Notable features:
- Uses the amazing `Leaflet`_ Javascript library for displaying maps,
with map data from `Mapbox`_
- Uses `Leaflet.Draw`_ for editing geographic information interactively,
including points, lines, and polygons
- Graceful fallback to editing `GeoJSON`_ data in a ``<textarea>``, if the
user has turned off Javascript
- Works with a `Geometry`_ SQL field that is integrated with `Shapely`_ objects
Getting Started
---------------
GeoAlchemy is based on SQLAlchemy, so you'll need to complete the "getting started"
directions for SQLAlchemy backend first. For GeoAlchemy, you'll also need a
map ID from `Mapbox`_, a map tile provider. (Don't worry, their basic plan
is free, and works quite well.) Then, just include the map ID in your application
settings::
app = Flask(__name__)
app.config['MAPBOX_MAP_ID'] = "example.abc123"
.. note::
Leaflet supports loading map tiles from any arbitrary map tile provider, but
at the moment, Flask-Admin only supports Mapbox. If you want to use other
providers, make a pull request!
Creating simple model
---------------------
GeoAlchemy comes with a `Geometry`_ field that is carefully divorced from the
`Shapely`_ library. Flask-Admin takes the approach that if you're using spatial
objects in your database, and you want an admin interface to edit those objects,
you're probably already using Shapely, so we provide a Geometry field that is
integrated with Shapely objects. To make your admin interface works, be sure to
use this field rather that the one that ships with GeoAlchemy when defining your
models::
from flask.ext.admin.contrib.geoa.sqltypes import Geometry
# .. flask initialization
db = SQLAlchemy(app)
class Location(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
point = db.Column(Geometry("POINT"))
if __name__ == '__main__':
admin = Admin(app)
admin.add_view(ModelView(User, db.session))
db.create_all()
app.run('0.0.0.0', 8000)
Limitations
-----------
There's currently no way to sort, filter, or search on geometric fields
in the admin. It's not clear that there's a good way to do so.
If you have any ideas or suggestions, make a pull request!
.. _GeoAlchemy: http://geoalchemy-2.readthedocs.org/
.. _Leaflet: http://leafletjs.com/
.. _Leaflet.Draw: https://github.com/Leaflet/Leaflet.draw
.. _Shapely: http://toblerity.org/shapely/
.. _Mapbox: https://www.mapbox.com/
.. _GeoJSON: http://geojson.org/
.. _Geometry: http://geoalchemy-2.readthedocs.org/en/latest/types.html#geoalchemy2.types.Geometry
......@@ -2,7 +2,7 @@ import os.path as op
from functools import wraps
from flask import Blueprint, render_template, abort, g, url_for
from flask import Blueprint, current_app, render_template, abort, g, url_for
from flask.ext.admin import babel
from flask.ext.admin._compat import with_metaclass
from flask.ext.admin import helpers as h
......@@ -275,6 +275,9 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
# Expose get_url helper
kwargs['get_url'] = self.get_url
# Expose config info
kwargs['config'] = current_app.config
# Contribute extra arguments
kwargs.update(self._template_args)
......
try:
import geoalchemy2
import shapely
except ImportError:
raise Exception('Please install geoalchemy2 and shapely in order to use geoalchemy integration')
from .view import ModelView
import json
from wtforms.fields import TextAreaField
from shapely.geometry import shape, mapping
from .widgets import LeafletWidget
class JSONField(TextAreaField):
def _value(self):
if self.raw_data:
return self.raw_data[0]
if self.data:
return self.to_json(self.data)
return ""
def process_formdata(self, valuelist):
if valuelist:
value = valuelist[0]
if not value:
self.data = None
return
try:
self.data = self.from_json(value)
except ValueError:
self.data = None
raise ValueError(self.gettext('Invalid JSON'))
def to_json(self, obj):
return json.dumps(obj)
def from_json(self, data):
return json.loads(data)
class GeoJSONField(JSONField):
widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", **kwargs):
super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.geometry_type = geometry_type.upper()
def _value(self):
if self.raw_data:
return self.raw_data[0]
if self.data:
self.data = mapping(self.data)
return super(GeoJSONField, self)._value()
def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist)
if self.data:
self.data = shape(self.data)
from flask.ext.admin.model.form import converts
from flask.ext.admin.contrib.sqla.form import AdminModelConverter as SQLAAdminConverter
from .fields import GeoJSONField
class AdminModelConverter(SQLAAdminConverter):
@converts('Geometry')
def convert_geom(self, column, field_args, **extra):
field_args['geometry_type'] = column.type.geometry_type
return GeoJSONField(**field_args)
from geoalchemy2 import Geometry as BaseGeometry
from geoalchemy2.shape import to_shape
class Geometry(BaseGeometry):
"""
PostGIS datatype that can convert directly to/from Shapely objects,
without worrying about WKTElements or WKBElements.
"""
def result_processor(self, dialect, coltype):
to_wkbelement = super(Geometry, self).result_processor(dialect, coltype)
def process(value):
if value:
return to_shape(to_wkbelement(value))
else:
return None
return process
def bind_processor(self, dialect):
from_wktelement = super(Geometry, self).bind_processor(dialect)
def process(value):
if value:
return from_wktelement(value.wkt)
else:
return None
return process
from flask.ext.admin.contrib.sqla.typefmt import DEFAULT_FORMATTERS as BASE_FORMATTERS
import json
from jinja2 import Markup
from wtforms.widgets import html_params
from shapely.geometry import mapping
from shapely.geometry.base import BaseGeometry
def geom_formatter(view, value):
params = html_params(**{
"data-role": "leaflet",
"disabled": "disabled",
"data-width": 100,
"data-height": 70,
"data-geometry-type": value.geom_type,
"data-zoom": 15,
})
geojson = json.dumps(mapping(value))
return Markup('<textarea %s>%s</textarea>' % (params, geojson))
DEFAULT_FORMATTERS = BASE_FORMATTERS.copy()
DEFAULT_FORMATTERS[BaseGeometry] = geom_formatter
from flask.ext.admin.contrib.sqla import ModelView as SQLAModelView
from flask.ext.admin.contrib.geoa import form, typefmt
class ModelView(SQLAModelView):
model_form_converter = form.AdminModelConverter
column_type_formatters = typefmt.DEFAULT_FORMATTERS
from wtforms.widgets import TextArea
def lat(pt):
return getattr(pt, "lat", getattr(pt, "x", pt[0]))
def lng(pt):
return getattr(pt, "lng", getattr(pt, "y", pt[1]))
class LeafletWidget(TextArea):
"""
`Leaflet <http://leafletjs.com/>`_ styled map widget. Inherits from
`TextArea` so that geographic data can be stored via the <textarea>
(and edited there if the user's browser does not have Javascript).
You must include leaflet.js, form.js and leaflet stylesheet for it to
work. You also need leaflet.draw.js (and its stylesheet) for it to be
editable.
"""
def __init__(
self, width=300, height=300, center=None,
zoom=None, min_zoom=None, max_zoom=None, max_bounds=None):
self.width = width
self.height = height
self.center = center
self.zoom = zoom
self.min_zoom = min_zoom
self.max_zoom = max_zoom
self.max_bounds = max_bounds
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', 'leaflet')
gtype = getattr(field, "geometry_type", "GEOMETRY")
kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor
if self.width:
kwargs["data-width"] = self.width
if self.height:
kwargs["data-height"] = self.height
if self.center:
kwargs["data-lat"] = lat(self.center)
kwargs["data-lng"] = lng(self.center)
if self.zoom:
kwargs["data-zoom"] = self.zoom
if self.min_zoom:
kwargs["data-min-zoom"] = self.min_zoom
if self.max_zoom:
kwargs["data-max-zoom"] = self.max_zoom
if self.max_bounds:
if getattr(self.max_bounds, "bounds"):
# this is a Shapely geometric object
minx, miny, maxx, maxy = self.max_bounds.bounds
elif len(self.max_bounds) == 4:
# this is a list of four values
minx, miny, maxx, maxy = self.max_bounds
else:
# this is a list of two points
minx = lat(self.max_bounds[0])
miny = lng(self.max_bounds[0])
maxx = lat(self.max_bounds[1])
maxy = lng(self.max_bounds[1])
kwargs["data-max-bounds-sw-lat"] = minx
kwargs["data-max-bounds-sw-lng"] = miny
kwargs["data-max-bounds-ne-lat"] = maxx
kwargs["data-max-bounds-ne-lng"] = maxy
return super(LeafletWidget, self).__call__(field, **kwargs)
try:
import pymongo
except ImportError:
raise Exception('Please install pymongo in order to use peewee integration')
raise Exception('Please install pymongo in order to use pymongo integration')
from .view import ModelView
......@@ -400,7 +400,7 @@ class BaseModelView(BaseView, ActionsMixin):
}
Changing the format of a DateTimeField will require changes to both form_widget_args and form_args.
Example::
form_args = dict(
......@@ -1197,7 +1197,11 @@ class BaseModelView(BaseView, ActionsMixin):
if choices_map:
return choices_map.get(value) or value
type_fmt = self.column_type_formatters.get(type(value))
type_fmt = None
for typeobj, formatter in self.column_type_formatters.items():
if isinstance(value, typeobj):
type_fmt = formatter
break
if type_fmt is not None:
value = type_fmt(self, value)
......
......@@ -69,6 +69,163 @@
$el.select2(opts);
}
/**
* Process Leaflet (map) widget
*/
function processLeafletWidget($el, name) {
if (!window.MAPBOX_MAP_ID) {
console.error("You must set MAPBOX_MAP_ID in your Flask settings to use the map widget");
return false;
}
var geometryType = $el.data("geometry-type")
if (geometryType) {
geometryType = geometryType.toUpperCase();
} else {
geometryType = "GEOMETRY";
}
var multiple = geometryType.lastIndexOf("MULTI", geometryType) === 0;
var editable = ! $el.is(":disabled");
var $map = $("<div>").width($el.data("width")).height($el.data("height"));
$el.after($map).hide();
var center = null;
if($el.data("lat") && $el.data("lng")) {
center = L.latLng($el.data("lat"), $el.data("lng"));
}
var maxBounds = null;
if ($el.data("max-bounds-sw-lat") && $el.data("max-bounds-sw-lng") &&
$el.data("max-bounds-ne-lat") && $el.data("max-bounds-ne-lng"))
{
maxBounds = L.latLngBounds(
L.latLng($el.data("max-bounds-sw-lat"), $el.data("max-bounds-sw-lng")),
L.latLng($el.data("max-bounds-ne-lat"), $el.data("max-bounds-ne-lng"))
)
}
var editableLayers;
if ($el.val()) {
editableLayers = new L.geoJson(JSON.parse($el.val()));
center = center || editableLayers.getBounds().getCenter();
} else {
editableLayers = new L.geoJson();
}
var mapOptions = {
center: center,
zoom: $el.data("zoom") || 12,
minZoom: $el.data("min-zoom"),
maxZoom: $el.data("max-zoom"),
maxBounds: maxBounds
}
if (!editable) {
mapOptions.dragging = false;
mapOptions.touchzoom = false;
mapOptions.scrollWheelZoom = false;
mapOptions.doubleClickZoom = false;
mapOptions.boxZoom = false;
mapOptions.tap = false;
mapOptions.keyboard = false;
mapOptions.zoomControl = false;
}
// only show attributions if the map is big enough
// (otherwise, it gets in the way)
if ($map.width() * $map.height() < 10000) {
mapOptions.attributionControl = false;
}
var map = L.map($map.get(0), mapOptions)
map.addLayer(editableLayers);
if (center) {
// if we have more than one point, make the map show everything
var bounds = editableLayers.getBounds()
if (!bounds.getNorthEast().equals(bounds.getSouthWest())) {
map.fitBounds(bounds);
}
} else {
// look up user's location by IP address
$.getJSON("http://ip-api.com/json/?callback=?", function(data) {
map.setView([data["lat"], data["lon"]], 12);
});
}
// set up tiles
L.tileLayer('http://{s}.tiles.mapbox.com/v3/'+MAPBOX_MAP_ID+'/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
maxZoom: 18
}).addTo(map);
// everything below here is to set up editing, so if we're not editable,
// we can just return early.
if (!editable) {
return true;
}
// set up Leaflet.draw editor
var drawOptions = {
draw: {
// circles are not geometries in geojson
circle: false
},
edit: {
featureGroup: editableLayers
}
}
if ($.inArray(geometryType, ["POINT", "MULTIPOINT"]) > -1) {
drawOptions.draw.polyline = false;
drawOptions.draw.polygon = false;
drawOptions.draw.rectangle = false;
} else if ($.inArray(geometryType, ["LINESTRING", "MULTILINESTRING"]) > -1) {
drawOptions.draw.marker = false;
drawOptions.draw.polygon = false;
drawOptions.draw.rectangle = false;
} else if ($.inArray(geometryType, ["POLYGON", "MULTIPOLYGON"]) > -1) {
drawOptions.draw.marker = false;
drawOptions.draw.polyline = false;
}
var drawControl = new L.Control.Draw(drawOptions);
map.addControl(drawControl);
// save when the editableLayers are edited
var saveToTextArea = function() {
var geo = editableLayers.toGeoJSON();
if (geo.features.length === 0) {
$el.val("");
return true
}
if (multiple) {
var coords = $.map(geo.features, function(feature) {
return [feature.geometry.coordinates];
})
geo = {
"type": geometryType,
"coordinates": coords
}
} else {
geo = geo.features[0].geometry;
}
$el.val(JSON.stringify(geo));
}
// handle creation
map.on('draw:created', function (e) {
if (!multiple) {
editableLayers.clearLayers();
}
editableLayers.addLayer(e.layer);
saveToTextArea();
})
map.on('draw:edited', saveToTextArea);
map.on('draw:deleted', saveToTextArea);
}
/**
* Process data-role attribute for the given input element. Feel free to override
*
......@@ -197,6 +354,9 @@
$container.find('.calendar-date').remove();
});
return true;
case 'leaflet':
processLeafletWidget($el, name);
return true;
}
};
......
/* required styles */
.leaflet-map-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-pane,
.leaflet-tile-container,
.leaflet-overlay-pane,
.leaflet-shadow-pane,
.leaflet-marker-pane,
.leaflet-popup-pane,
.leaflet-overlay-pane svg,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
-ms-touch-action: none;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container img {
max-width: none !important;
}
/* stupid Android 2 doesn't understand "max-width: none" properly */
.leaflet-container img.leaflet-image-layer {
max-width: 15000px !important;
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-tile-pane { z-index: 2; }
.leaflet-objects-pane { z-index: 3; }
.leaflet-overlay-pane { z-index: 4; }
.leaflet-shadow-pane { z-index: 5; }
.leaflet-marker-pane { z-index: 6; }
.leaflet-popup-pane { z-index: 7; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 7;
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile,
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-tile-loaded,
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile,
.leaflet-touching .leaflet-zoom-animated {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-clickable {
cursor: pointer;
}
.leaflet-container {
cursor: -webkit-grab;
cursor: -moz-grab;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-container,
.leaflet-dragging .leaflet-clickable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-control-zoom-out {
font-size: 20px;
}
.leaflet-touch .leaflet-control-zoom-in {
font-size: 22px;
}
.leaflet-touch .leaflet-control-zoom-out {
font-size: 24px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: content-box;
box-sizing: content-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
margin: 0 auto;
width: 40px;
height: 20px;
position: relative;
overflow: hidden;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* ================================================================== */
/* Toolbars
/* ================================================================== */
.leaflet-draw-section {
position: relative;
}
.leaflet-draw-toolbar {
margin-top: 12px;
}
.leaflet-draw-toolbar-top {
margin-top: 0;
}
.leaflet-draw-toolbar-notop a:first-child {
border-top-right-radius: 0;
}
.leaflet-draw-toolbar-nobottom a:last-child {
border-bottom-right-radius: 0;
}
.leaflet-draw-toolbar a {
background-image: url('images/spritesheet.png');
background-repeat: no-repeat;
}
.leaflet-retina .leaflet-draw-toolbar a {
background-image: url('images/spritesheet-2x.png');
background-size: 270px 30px;
}
.leaflet-draw a {
display: block;
text-align: center;
text-decoration: none;
}
/* ================================================================== */
/* Toolbar actions menu
/* ================================================================== */
.leaflet-draw-actions {
display: none;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
left: 26px; /* leaflet-draw-toolbar.left + leaflet-draw-toolbar.width */
top: 0;
white-space: nowrap;
}
.leaflet-right .leaflet-draw-actions {
right:26px;
left:auto;
}
.leaflet-draw-actions li {
display: inline-block;
}
.leaflet-draw-actions li:first-child a {
border-left: none;
}
.leaflet-draw-actions li:last-child a {
-webkit-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
}
.leaflet-right .leaflet-draw-actions li:last-child a {
-webkit-border-radius: 0;
border-radius: 0;
}
.leaflet-right .leaflet-draw-actions li:first-child a {
-webkit-border-radius: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
.leaflet-draw-actions a {
background-color: #919187;
border-left: 1px solid #AAA;
color: #FFF;
font: 11px/19px "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 28px;
text-decoration: none;
padding-left: 10px;
padding-right: 10px;
height: 28px;
}
.leaflet-draw-actions-bottom {
margin-top: 0;
}
.leaflet-draw-actions-top {
margin-top: 1px;
}
.leaflet-draw-actions-top a,
.leaflet-draw-actions-bottom a {
height: 27px;
line-height: 27px;
}
.leaflet-draw-actions a:hover {
background-color: #A0A098;
}
.leaflet-draw-actions-top.leaflet-draw-actions-bottom a {
height: 26px;
line-height: 26px;
}
/* ================================================================== */
/* Draw toolbar
/* ================================================================== */
.leaflet-draw-toolbar .leaflet-draw-draw-polyline {
background-position: -2px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-polygon {
background-position: -31px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-rectangle {
background-position: -62px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-circle {
background-position: -92px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-marker {
background-position: -122px -2px;
}
/* ================================================================== */
/* Edit toolbar
/* ================================================================== */
.leaflet-draw-toolbar .leaflet-draw-edit-edit {
background-position: -152px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-remove {
background-position: -182px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled {
background-position: -212px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
background-position: -242px -2px;
}
/* ================================================================== */
/* Drawing styles
/* ================================================================== */
.leaflet-mouse-marker {
background-color: #fff;
cursor: crosshair;
}
.leaflet-draw-tooltip {
background: rgb(54, 54, 54);
background: rgba(0, 0, 0, 0.5);
border: 1px solid transparent;
-webkit-border-radius: 4px;
border-radius: 4px;
color: #fff;
font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif;
margin-left: 20px;
margin-top: -21px;
padding: 4px 8px;
position: absolute;
visibility: hidden;
white-space: nowrap;
z-index: 6;
}
.leaflet-draw-tooltip:before {
border-right: 6px solid black;
border-right-color: rgba(0, 0, 0, 0.5);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
content: "";
position: absolute;
top: 7px;
left: -7px;
}
.leaflet-error-draw-tooltip {
background-color: #F2DEDE;
border: 1px solid #E6B6BD;
color: #B94A48;
}
.leaflet-error-draw-tooltip:before {
border-right-color: #E6B6BD;
}
.leaflet-draw-tooltip-single {
margin-top: -12px
}
.leaflet-draw-tooltip-subtext {
color: #f8d5e4;
}
.leaflet-draw-guide-dash {
font-size: 1%;
opacity: 0.6;
position: absolute;
width: 5px;
height: 5px;
}
/* ================================================================== */
/* Edit styles
/* ================================================================== */
.leaflet-edit-marker-selected {
background: rgba(254, 87, 161, 0.1);
border: 4px dashed rgba(254, 87, 161, 0.6);
-webkit-border-radius: 4px;
border-radius: 4px;
}
.leaflet-edit-move {
cursor: move;
}
.leaflet-edit-resize {
cursor: pointer;
}
/* ================================================================== */
/* Old IE styles
/* ================================================================== */
.leaflet-oldie .leaflet-draw-toolbar {
border: 3px solid #999;
}
.leaflet-oldie .leaflet-draw-toolbar a {
background-color: #eee;
}
.leaflet-oldie .leaflet-draw-toolbar a:hover {
background-color: #fff;
}
.leaflet-oldie .leaflet-draw-actions {
left: 32px;
margin-top: 3px;
}
.leaflet-oldie .leaflet-draw-actions li {
display: inline;
zoom: 1;
}
.leaflet-oldie .leaflet-edit-marker-selected {
border: 4px dashed #fe93c2;
}
.leaflet-oldie .leaflet-draw-actions a {
background-color: #999;
}
.leaflet-oldie .leaflet-draw-actions a:hover {
background-color: #a5a5a5;
}
.leaflet-oldie .leaflet-draw-actions-top a {
margin-top: 1px;
}
.leaflet-oldie .leaflet-draw-actions-bottom a {
height: 28px;
line-height: 28px;
}
.leaflet-oldie .leaflet-draw-actions-top.leaflet-draw-actions-bottom a {
height: 27px;
line-height: 27px;
}
/*
Leaflet.draw, a plugin that adds drawing and editing tools to Leaflet powered maps.
(c) 2012-2013, Jacob Toye, Smartrak
https://github.com/Leaflet/Leaflet.draw
http://leafletjs.com
https://github.com/jacobtoye
*/
!function(t,e){L.drawVersion="0.2.3",L.drawLocal={draw:{toolbar:{actions:{title:"Cancel drawing",text:"Cancel"},undo:{title:"Delete last point drawn",text:"Delete last point"},buttons:{polyline:"Draw a polyline",polygon:"Draw a polygon",rectangle:"Draw a rectangle",circle:"Draw a circle",marker:"Draw a marker"}},handlers:{circle:{tooltip:{start:"Click and drag to draw circle."}},marker:{tooltip:{start:"Click map to place marker."}},polygon:{tooltip:{start:"Click to start drawing shape.",cont:"Click to continue drawing shape.",end:"Click first point to close this shape."}},polyline:{error:"<strong>Error:</strong> shape edges cannot cross!",tooltip:{start:"Click to start drawing line.",cont:"Click to continue drawing line.",end:"Click last point to finish line."}},rectangle:{tooltip:{start:"Click and drag to draw rectangle."}},simpleshape:{tooltip:{end:"Release mouse to finish drawing."}}}},edit:{toolbar:{actions:{save:{title:"Save changes.",text:"Save"},cancel:{title:"Cancel editing, discards all changes.",text:"Cancel"}},buttons:{edit:"Edit layers.",editDisabled:"No layers to edit.",remove:"Delete layers.",removeDisabled:"No layers to delete."}},handlers:{edit:{tooltip:{text:"Drag handles, or marker to edit feature.",subtext:"Click cancel to undo changes."}},remove:{tooltip:{text:"Click on a feature to remove"}}}}},L.Draw={},L.Draw.Feature=L.Handler.extend({includes:L.Mixin.Events,initialize:function(t,e){this._map=t,this._container=t._container,this._overlayPane=t._panes.overlayPane,this._popupPane=t._panes.popupPane,e&&e.shapeOptions&&(e.shapeOptions=L.Util.extend({},this.options.shapeOptions,e.shapeOptions)),L.setOptions(this,e)},enable:function(){this._enabled||(this.fire("enabled",{handler:this.type}),this._map.fire("draw:drawstart",{layerType:this.type}),L.Handler.prototype.enable.call(this))},disable:function(){this._enabled&&(L.Handler.prototype.disable.call(this),this._map.fire("draw:drawstop",{layerType:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(L.DomUtil.disableTextSelection(),t.getContainer().focus(),this._tooltip=new L.Tooltip(this._map),L.DomEvent.on(this._container,"keyup",this._cancelDrawing,this))},removeHooks:function(){this._map&&(L.DomUtil.enableTextSelection(),this._tooltip.dispose(),this._tooltip=null,L.DomEvent.off(this._container,"keyup",this._cancelDrawing,this))},setOptions:function(t){L.setOptions(this,t)},_fireCreatedEvent:function(t){this._map.fire("draw:created",{layer:t,layerType:this.type})},_cancelDrawing:function(t){27===t.keyCode&&this.disable()}}),L.Draw.Polyline=L.Draw.Feature.extend({statics:{TYPE:"polyline"},Poly:L.Polyline,options:{allowIntersection:!0,repeatMode:!1,drawError:{color:"#b00b00",timeout:2500},icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"}),guidelineDistance:20,maxGuideLineLength:4e3,shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!1,clickable:!0},metric:!0,showLength:!0,zIndexOffset:2e3},initialize:function(t,e){this.options.drawError.message=L.drawLocal.draw.handlers.polyline.error,e&&e.drawError&&(e.drawError=L.Util.extend({},this.options.drawError,e.drawError)),this.type=L.Draw.Polyline.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._markers=[],this._markerGroup=new L.LayerGroup,this._map.addLayer(this._markerGroup),this._poly=new L.Polyline([],this.options.shapeOptions),this._tooltip.updateContent(this._getTooltipText()),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("mousedown",this._onMouseDown,this).addTo(this._map),this._map.on("mousemove",this._onMouseMove,this).on("mouseup",this._onMouseUp,this).on("zoomend",this._onZoomEnd,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._clearHideErrorTimeout(),this._cleanUpShape(),this._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers,this._map.removeLayer(this._poly),delete this._poly,this._mouseMarker.off("mousedown",this._onMouseDown,this).off("mouseup",this._onMouseUp,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._clearGuides(),this._map.off("mousemove",this._onMouseMove,this).off("zoomend",this._onZoomEnd,this)},deleteLastVertex:function(){if(!(this._markers.length<=1)){var t=this._markers.pop(),e=this._poly,i=this._poly.spliceLatLngs(e.getLatLngs().length-1,1)[0];this._markerGroup.removeLayer(t),e.getLatLngs().length<2&&this._map.removeLayer(e),this._vertexChanged(i,!1)}},addVertex:function(t){var e=this._markers.length;return e>0&&!this.options.allowIntersection&&this._poly.newLatLngIntersects(t)?(this._showErrorTooltip(),void 0):(this._errorShown&&this._hideErrorTooltip(),this._markers.push(this._createMarker(t)),this._poly.addLatLng(t),2===this._poly.getLatLngs().length&&this._map.addLayer(this._poly),this._vertexChanged(t,!0),void 0)},_finishShape:function(){var t=this._poly.newLatLngIntersects(this._poly.getLatLngs()[0],!0);return!this.options.allowIntersection&&t||!this._shapeIsValid()?(this._showErrorTooltip(),void 0):(this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable(),void 0)},_shapeIsValid:function(){return!0},_onZoomEnd:function(){this._updateGuide()},_onMouseMove:function(t){var e=t.layerPoint,i=t.latlng;this._currentLatLng=i,this._updateTooltip(i),this._updateGuide(e),this._mouseMarker.setLatLng(i),L.DomEvent.preventDefault(t.originalEvent)},_vertexChanged:function(t,e){this._updateFinishHandler(),this._updateRunningMeasure(t,e),this._clearGuides(),this._updateTooltip()},_onMouseDown:function(t){var e=t.originalEvent;this._mouseDownOrigin=L.point(e.clientX,e.clientY)},_onMouseUp:function(e){if(this._mouseDownOrigin){var i=L.point(e.originalEvent.clientX,e.originalEvent.clientY).distanceTo(this._mouseDownOrigin);Math.abs(i)<9*(t.devicePixelRatio||1)&&this.addVertex(e.latlng)}this._mouseDownOrigin=null},_updateFinishHandler:function(){var t=this._markers.length;t>1&&this._markers[t-1].on("click",this._finishShape,this),t>2&&this._markers[t-2].off("click",this._finishShape,this)},_createMarker:function(t){var e=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset});return this._markerGroup.addLayer(e),e},_updateGuide:function(t){var e=this._markers.length;e>0&&(t=t||this._map.latLngToLayerPoint(this._currentLatLng),this._clearGuides(),this._drawGuide(this._map.latLngToLayerPoint(this._markers[e-1].getLatLng()),t))},_updateTooltip:function(t){var e=this._getTooltipText();t&&this._tooltip.updatePosition(t),this._errorShown||this._tooltip.updateContent(e)},_drawGuide:function(t,e){var i,o,a,s=Math.floor(Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))),r=this.options.guidelineDistance,n=this.options.maxGuideLineLength,l=s>n?s-n:r;for(this._guidesContainer||(this._guidesContainer=L.DomUtil.create("div","leaflet-draw-guides",this._overlayPane));s>l;l+=this.options.guidelineDistance)i=l/s,o={x:Math.floor(t.x*(1-i)+i*e.x),y:Math.floor(t.y*(1-i)+i*e.y)},a=L.DomUtil.create("div","leaflet-draw-guide-dash",this._guidesContainer),a.style.backgroundColor=this._errorShown?this.options.drawError.color:this.options.shapeOptions.color,L.DomUtil.setPosition(a,o)},_updateGuideColor:function(t){if(this._guidesContainer)for(var e=0,i=this._guidesContainer.childNodes.length;i>e;e++)this._guidesContainer.childNodes[e].style.backgroundColor=t},_clearGuides:function(){if(this._guidesContainer)for(;this._guidesContainer.firstChild;)this._guidesContainer.removeChild(this._guidesContainer.firstChild)},_getTooltipText:function(){var t,e,i=this.options.showLength;return 0===this._markers.length?t={text:L.drawLocal.draw.handlers.polyline.tooltip.start}:(e=i?this._getMeasurementString():"",t=1===this._markers.length?{text:L.drawLocal.draw.handlers.polyline.tooltip.cont,subtext:e}:{text:L.drawLocal.draw.handlers.polyline.tooltip.end,subtext:e}),t},_updateRunningMeasure:function(t,e){var i,o,a=this._markers.length;1===this._markers.length?this._measurementRunningTotal=0:(i=a-(e?2:1),o=t.distanceTo(this._markers[i].getLatLng()),this._measurementRunningTotal+=o*(e?1:-1))},_getMeasurementString:function(){var t,e=this._currentLatLng,i=this._markers[this._markers.length-1].getLatLng();return t=this._measurementRunningTotal+e.distanceTo(i),L.GeometryUtil.readableDistance(t,this.options.metric)},_showErrorTooltip:function(){this._errorShown=!0,this._tooltip.showAsError().updateContent({text:this.options.drawError.message}),this._updateGuideColor(this.options.drawError.color),this._poly.setStyle({color:this.options.drawError.color}),this._clearHideErrorTimeout(),this._hideErrorTimeout=setTimeout(L.Util.bind(this._hideErrorTooltip,this),this.options.drawError.timeout)},_hideErrorTooltip:function(){this._errorShown=!1,this._clearHideErrorTimeout(),this._tooltip.removeError().updateContent(this._getTooltipText()),this._updateGuideColor(this.options.shapeOptions.color),this._poly.setStyle({color:this.options.shapeOptions.color})},_clearHideErrorTimeout:function(){this._hideErrorTimeout&&(clearTimeout(this._hideErrorTimeout),this._hideErrorTimeout=null)},_cleanUpShape:function(){this._markers.length>1&&this._markers[this._markers.length-1].off("click",this._finishShape,this)},_fireCreatedEvent:function(){var t=new this.Poly(this._poly.getLatLngs(),this.options.shapeOptions);L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.Polygon=L.Draw.Polyline.extend({statics:{TYPE:"polygon"},Poly:L.Polygon,options:{showArea:!1,shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0}},initialize:function(t,e){L.Draw.Polyline.prototype.initialize.call(this,t,e),this.type=L.Draw.Polygon.TYPE},_updateFinishHandler:function(){var t=this._markers.length;1===t&&this._markers[0].on("click",this._finishShape,this),t>2&&(this._markers[t-1].on("dblclick",this._finishShape,this),t>3&&this._markers[t-2].off("dblclick",this._finishShape,this))},_getTooltipText:function(){var t,e;return 0===this._markers.length?t=L.drawLocal.draw.handlers.polygon.tooltip.start:this._markers.length<3?t=L.drawLocal.draw.handlers.polygon.tooltip.cont:(t=L.drawLocal.draw.handlers.polygon.tooltip.end,e=this._getMeasurementString()),{text:t,subtext:e}},_getMeasurementString:function(){var t=this._area;return t?L.GeometryUtil.readableArea(t,this.options.metric):null},_shapeIsValid:function(){return this._markers.length>=3},_vertexAdded:function(){if(!this.options.allowIntersection&&this.options.showArea){var t=this._poly.getLatLngs();this._area=L.GeometryUtil.geodesicArea(t)}},_cleanUpShape:function(){var t=this._markers.length;t>0&&(this._markers[0].off("click",this._finishShape,this),t>2&&this._markers[t-1].off("dblclick",this._finishShape,this))}}),L.SimpleShape={},L.Draw.SimpleShape=L.Draw.Feature.extend({options:{repeatMode:!1},initialize:function(t,e){this._endLabelText=L.drawLocal.draw.handlers.simpleshape.tooltip.end,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._mapDraggable=this._map.dragging.enabled(),this._mapDraggable&&this._map.dragging.disable(),this._container.style.cursor="crosshair",this._tooltip.updateContent({text:this._initialLabelText}),this._map.on("mousedown",this._onMouseDown,this).on("mousemove",this._onMouseMove,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._mapDraggable&&this._map.dragging.enable(),this._container.style.cursor="",this._map.off("mousedown",this._onMouseDown,this).off("mousemove",this._onMouseMove,this),L.DomEvent.off(e,"mouseup",this._onMouseUp,this),this._shape&&(this._map.removeLayer(this._shape),delete this._shape)),this._isDrawing=!1},_onMouseDown:function(t){this._isDrawing=!0,this._startLatLng=t.latlng,L.DomEvent.on(e,"mouseup",this._onMouseUp,this).preventDefault(t.originalEvent)},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._isDrawing&&(this._tooltip.updateContent({text:this._endLabelText}),this._drawShape(e))},_onMouseUp:function(){this._shape&&this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()}}),L.Draw.Rectangle=L.Draw.SimpleShape.extend({statics:{TYPE:"rectangle"},options:{shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0}},initialize:function(t,e){this.type=L.Draw.Rectangle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.rectangle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},_drawShape:function(t){this._shape?this._shape.setBounds(new L.LatLngBounds(this._startLatLng,t)):(this._shape=new L.Rectangle(new L.LatLngBounds(this._startLatLng,t),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Rectangle(this._shape.getBounds(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.Circle=L.Draw.SimpleShape.extend({statics:{TYPE:"circle"},options:{shapeOptions:{stroke:!0,color:"#f06eaa",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},showRadius:!0,metric:!0},initialize:function(t,e){this.type=L.Draw.Circle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.circle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},_drawShape:function(t){this._shape?this._shape.setRadius(this._startLatLng.distanceTo(t)):(this._shape=new L.Circle(this._startLatLng,this._startLatLng.distanceTo(t),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Circle(this._startLatLng,this._shape.getRadius(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_onMouseMove:function(t){var e,i=t.latlng,o=this.options.showRadius,a=this.options.metric;this._tooltip.updatePosition(i),this._isDrawing&&(this._drawShape(i),e=this._shape.getRadius().toFixed(1),this._tooltip.updateContent({text:this._endLabelText,subtext:o?"Radius: "+L.GeometryUtil.readableDistance(e,a):""}))}}),L.Draw.Marker=L.Draw.Feature.extend({statics:{TYPE:"marker"},options:{icon:new L.Icon.Default,repeatMode:!1,zIndexOffset:2e3},initialize:function(t,e){this.type=L.Draw.Marker.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._tooltip.updateContent({text:L.drawLocal.draw.handlers.marker.tooltip.start}),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("click",this._onClick,this).addTo(this._map),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._marker&&(this._marker.off("click",this._onClick,this),this._map.off("click",this._onClick,this).removeLayer(this._marker),delete this._marker),this._mouseMarker.off("click",this._onClick,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._map.off("mousemove",this._onMouseMove,this))},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._mouseMarker.setLatLng(e),this._marker?(e=this._mouseMarker.getLatLng(),this._marker.setLatLng(e)):(this._marker=new L.Marker(e,{icon:this.options.icon,zIndexOffset:this.options.zIndexOffset}),this._marker.on("click",this._onClick,this),this._map.on("click",this._onClick,this).addLayer(this._marker))},_onClick:function(){this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()},_fireCreatedEvent:function(){var t=new L.Marker(this._marker.getLatLng(),{icon:this.options.icon});L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Edit=L.Edit||{},L.Edit.Poly=L.Handler.extend({options:{icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"})},initialize:function(t,e){this._poly=t,L.setOptions(this,e)},addHooks:function(){this._poly._map&&(this._markerGroup||this._initMarkers(),this._poly._map.addLayer(this._markerGroup))},removeHooks:function(){this._poly._map&&(this._poly._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers)},updateMarkers:function(){this._markerGroup.clearLayers(),this._initMarkers()},_initMarkers:function(){this._markerGroup||(this._markerGroup=new L.LayerGroup),this._markers=[];var t,e,i,o,a=this._poly._latlngs;for(t=0,i=a.length;i>t;t++)o=this._createMarker(a[t],t),o.on("click",this._onMarkerClick,this),this._markers.push(o);var s,r;for(t=0,e=i-1;i>t;e=t++)(0!==t||L.Polygon&&this._poly instanceof L.Polygon)&&(s=this._markers[e],r=this._markers[t],this._createMiddleMarker(s,r),this._updatePrevNext(s,r))},_createMarker:function(t,e){var i=new L.Marker(t,{draggable:!0,icon:this.options.icon});return i._origLatLng=t,i._index=e,i.on("drag",this._onMarkerDrag,this),i.on("dragend",this._fireEdit,this),this._markerGroup.addLayer(i),i},_removeMarker:function(t){var e=t._index;this._markerGroup.removeLayer(t),this._markers.splice(e,1),this._poly.spliceLatLngs(e,1),this._updateIndexes(e,-1),t.off("drag",this._onMarkerDrag,this).off("dragend",this._fireEdit,this).off("click",this._onMarkerClick,this)},_fireEdit:function(){this._poly.edited=!0,this._poly.fire("edit")},_onMarkerDrag:function(t){var e=t.target;L.extend(e._origLatLng,e._latlng),e._middleLeft&&e._middleLeft.setLatLng(this._getMiddleLatLng(e._prev,e)),e._middleRight&&e._middleRight.setLatLng(this._getMiddleLatLng(e,e._next)),this._poly.redraw()},_onMarkerClick:function(t){var e=L.Polygon&&this._poly instanceof L.Polygon?4:3,i=t.target;this._poly._latlngs.length<e||(this._removeMarker(i),this._updatePrevNext(i._prev,i._next),i._middleLeft&&this._markerGroup.removeLayer(i._middleLeft),i._middleRight&&this._markerGroup.removeLayer(i._middleRight),i._prev&&i._next?this._createMiddleMarker(i._prev,i._next):i._prev?i._next||(i._prev._middleRight=null):i._next._middleLeft=null,this._fireEdit())},_updateIndexes:function(t,e){this._markerGroup.eachLayer(function(i){i._index>t&&(i._index+=e)})},_createMiddleMarker:function(t,e){var i,o,a,s=this._getMiddleLatLng(t,e),r=this._createMarker(s);r.setOpacity(.6),t._middleRight=e._middleLeft=r,o=function(){var o=e._index;r._index=o,r.off("click",i,this).on("click",this._onMarkerClick,this),s.lat=r.getLatLng().lat,s.lng=r.getLatLng().lng,this._poly.spliceLatLngs(o,0,s),this._markers.splice(o,0,r),r.setOpacity(1),this._updateIndexes(o,1),e._index++,this._updatePrevNext(t,r),this._updatePrevNext(r,e),this._poly.fire("editstart")},a=function(){r.off("dragstart",o,this),r.off("dragend",a,this),this._createMiddleMarker(t,r),this._createMiddleMarker(r,e)},i=function(){o.call(this),a.call(this),this._fireEdit()},r.on("click",i,this).on("dragstart",o,this).on("dragend",a,this),this._markerGroup.addLayer(r)},_updatePrevNext:function(t,e){t&&(t._next=e),e&&(e._prev=t)},_getMiddleLatLng:function(t,e){var i=this._poly._map,o=i.project(t.getLatLng()),a=i.project(e.getLatLng());return i.unproject(o._add(a)._divideBy(2))}}),L.Polyline.addInitHook(function(){this.editing||(L.Edit.Poly&&(this.editing=new L.Edit.Poly(this),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()}))}),L.Edit=L.Edit||{},L.Edit.SimpleShape=L.Handler.extend({options:{moveIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move"}),resizeIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize"})},initialize:function(t,e){this._shape=t,L.Util.setOptions(this,e)},addHooks:function(){this._shape._map&&(this._map=this._shape._map,this._markerGroup||this._initMarkers(),this._map.addLayer(this._markerGroup))},removeHooks:function(){if(this._shape._map){this._unbindMarker(this._moveMarker);for(var t=0,e=this._resizeMarkers.length;e>t;t++)this._unbindMarker(this._resizeMarkers[t]);this._resizeMarkers=null,this._map.removeLayer(this._markerGroup),delete this._markerGroup}this._map=null},updateMarkers:function(){this._markerGroup.clearLayers(),this._initMarkers()},_initMarkers:function(){this._markerGroup||(this._markerGroup=new L.LayerGroup),this._createMoveMarker(),this._createResizeMarker()},_createMoveMarker:function(){},_createResizeMarker:function(){},_createMarker:function(t,e){var i=new L.Marker(t,{draggable:!0,icon:e,zIndexOffset:10});return this._bindMarker(i),this._markerGroup.addLayer(i),i},_bindMarker:function(t){t.on("dragstart",this._onMarkerDragStart,this).on("drag",this._onMarkerDrag,this).on("dragend",this._onMarkerDragEnd,this)},_unbindMarker:function(t){t.off("dragstart",this._onMarkerDragStart,this).off("drag",this._onMarkerDrag,this).off("dragend",this._onMarkerDragEnd,this)},_onMarkerDragStart:function(t){var e=t.target;e.setOpacity(0),this._shape.fire("editstart")},_fireEdit:function(){this._shape.edited=!0,this._shape.fire("edit")},_onMarkerDrag:function(t){var e=t.target,i=e.getLatLng();e===this._moveMarker?this._move(i):this._resize(i),this._shape.redraw()},_onMarkerDragEnd:function(t){var e=t.target;e.setOpacity(1),this._fireEdit()},_move:function(){},_resize:function(){}}),L.Edit=L.Edit||{},L.Edit.Rectangle=L.Edit.SimpleShape.extend({_createMoveMarker:function(){var t=this._shape.getBounds(),e=t.getCenter();this._moveMarker=this._createMarker(e,this.options.moveIcon)},_createResizeMarker:function(){var t=this._getCorners();this._resizeMarkers=[];for(var e=0,i=t.length;i>e;e++)this._resizeMarkers.push(this._createMarker(t[e],this.options.resizeIcon)),this._resizeMarkers[e]._cornerIndex=e},_onMarkerDragStart:function(t){L.Edit.SimpleShape.prototype._onMarkerDragStart.call(this,t);var e=this._getCorners(),i=t.target,o=i._cornerIndex;this._oppositeCorner=e[(o+2)%4],this._toggleCornerMarkers(0,o)},_onMarkerDragEnd:function(t){var e,i,o=t.target;o===this._moveMarker&&(e=this._shape.getBounds(),i=e.getCenter(),o.setLatLng(i)),this._toggleCornerMarkers(1),this._repositionCornerMarkers(),L.Edit.SimpleShape.prototype._onMarkerDragEnd.call(this,t)},_move:function(t){for(var e,i=this._shape.getLatLngs(),o=this._shape.getBounds(),a=o.getCenter(),s=[],r=0,n=i.length;n>r;r++)e=[i[r].lat-a.lat,i[r].lng-a.lng],s.push([t.lat+e[0],t.lng+e[1]]);this._shape.setLatLngs(s),this._repositionCornerMarkers()},_resize:function(t){var e;this._shape.setBounds(L.latLngBounds(t,this._oppositeCorner)),e=this._shape.getBounds(),this._moveMarker.setLatLng(e.getCenter())},_getCorners:function(){var t=this._shape.getBounds(),e=t.getNorthWest(),i=t.getNorthEast(),o=t.getSouthEast(),a=t.getSouthWest();return[e,i,o,a]},_toggleCornerMarkers:function(t){for(var e=0,i=this._resizeMarkers.length;i>e;e++)this._resizeMarkers[e].setOpacity(t)},_repositionCornerMarkers:function(){for(var t=this._getCorners(),e=0,i=this._resizeMarkers.length;i>e;e++)this._resizeMarkers[e].setLatLng(t[e])}}),L.Rectangle.addInitHook(function(){L.Edit.Rectangle&&(this.editing=new L.Edit.Rectangle(this),this.options.editable&&this.editing.enable())}),L.Edit=L.Edit||{},L.Edit.Circle=L.Edit.SimpleShape.extend({_createMoveMarker:function(){var t=this._shape.getLatLng();this._moveMarker=this._createMarker(t,this.options.moveIcon)},_createResizeMarker:function(){var t=this._shape.getLatLng(),e=this._getResizeMarkerPoint(t);this._resizeMarkers=[],this._resizeMarkers.push(this._createMarker(e,this.options.resizeIcon))},_getResizeMarkerPoint:function(t){var e=this._shape._radius*Math.cos(Math.PI/4),i=this._map.project(t);return this._map.unproject([i.x+e,i.y-e])},_move:function(t){var e=this._getResizeMarkerPoint(t);this._resizeMarkers[0].setLatLng(e),this._shape.setLatLng(t)},_resize:function(t){var e=this._moveMarker.getLatLng(),i=e.distanceTo(t);this._shape.setRadius(i)}}),L.Circle.addInitHook(function(){L.Edit.Circle&&(this.editing=new L.Edit.Circle(this),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()})}),L.LatLngUtil={cloneLatLngs:function(t){for(var e=[],i=0,o=t.length;o>i;i++)e.push(this.cloneLatLng(t[i]));return e},cloneLatLng:function(t){return L.latLng(t.lat,t.lng)}},L.GeometryUtil=L.extend(L.GeometryUtil||{},{geodesicArea:function(t){var e,i,o=t.length,a=0,s=L.LatLng.DEG_TO_RAD;if(o>2){for(var r=0;o>r;r++)e=t[r],i=t[(r+1)%o],a+=(i.lng-e.lng)*s*(2+Math.sin(e.lat*s)+Math.sin(i.lat*s));a=6378137*a*6378137/2}return Math.abs(a)},readableArea:function(t,e){var i;return e?i=t>=1e4?(1e-4*t).toFixed(2)+" ha":t.toFixed(2)+" m&sup2;":(t*=.836127,i=t>=3097600?(t/3097600).toFixed(2)+" mi&sup2;":t>=4840?(t/4840).toFixed(2)+" acres":Math.ceil(t)+" yd&sup2;"),i},readableDistance:function(t,e){var i;return e?i=t>1e3?(t/1e3).toFixed(2)+" km":Math.ceil(t)+" m":(t*=1.09361,i=t>1760?(t/1760).toFixed(2)+" miles":Math.ceil(t)+" yd"),i}}),L.Util.extend(L.LineUtil,{segmentsIntersect:function(t,e,i,o){return this._checkCounterclockwise(t,i,o)!==this._checkCounterclockwise(e,i,o)&&this._checkCounterclockwise(t,e,i)!==this._checkCounterclockwise(t,e,o)},_checkCounterclockwise:function(t,e,i){return(i.y-t.y)*(e.x-t.x)>(e.y-t.y)*(i.x-t.x)}}),L.Polyline.include({intersects:function(){var t,e,i,o=this._originalPoints,a=o?o.length:0;if(this._tooFewPointsForIntersection())return!1;for(t=a-1;t>=3;t--)if(e=o[t-1],i=o[t],this._lineSegmentsIntersectsRange(e,i,t-2))return!0;return!1},newLatLngIntersects:function(t,e){return this._map?this.newPointIntersects(this._map.latLngToLayerPoint(t),e):!1},newPointIntersects:function(t,e){var i=this._originalPoints,o=i?i.length:0,a=i?i[o-1]:null,s=o-2;return this._tooFewPointsForIntersection(1)?!1:this._lineSegmentsIntersectsRange(a,t,s,e?1:0)},_tooFewPointsForIntersection:function(t){var e=this._originalPoints,i=e?e.length:0;return i+=t||0,!this._originalPoints||3>=i},_lineSegmentsIntersectsRange:function(t,e,i,o){var a,s,r=this._originalPoints;o=o||0;for(var n=i;n>o;n--)if(a=r[n-1],s=r[n],L.LineUtil.segmentsIntersect(t,e,a,s))return!0;return!1}}),L.Polygon.include({intersects:function(){var t,e,i,o,a,s=this._originalPoints;return this._tooFewPointsForIntersection()?!1:(t=L.Polyline.prototype.intersects.call(this))?!0:(e=s.length,i=s[0],o=s[e-1],a=e-2,this._lineSegmentsIntersectsRange(o,i,a,1))}}),L.Control.Draw=L.Control.extend({options:{position:"topleft",draw:{},edit:!1},initialize:function(t){if(L.version<"0.7")throw new Error("Leaflet.draw 0.2.3+ requires Leaflet 0.7.0+. Download latest from https://github.com/Leaflet/Leaflet/");L.Control.prototype.initialize.call(this,t);var e,i;this._toolbars={},L.DrawToolbar&&this.options.draw&&(i=new L.DrawToolbar(this.options.draw),e=L.stamp(i),this._toolbars[e]=i,this._toolbars[e].on("enable",this._toolbarEnabled,this)),L.EditToolbar&&this.options.edit&&(i=new L.EditToolbar(this.options.edit),e=L.stamp(i),this._toolbars[e]=i,this._toolbars[e].on("enable",this._toolbarEnabled,this))},onAdd:function(t){var e,i=L.DomUtil.create("div","leaflet-draw"),o=!1,a="leaflet-draw-toolbar-top";for(var s in this._toolbars)this._toolbars.hasOwnProperty(s)&&(e=this._toolbars[s].addToolbar(t),e&&(o||(L.DomUtil.hasClass(e,a)||L.DomUtil.addClass(e.childNodes[0],a),o=!0),i.appendChild(e)));return i},onRemove:function(){for(var t in this._toolbars)this._toolbars.hasOwnProperty(t)&&this._toolbars[t].removeToolbar()},setDrawingOptions:function(t){for(var e in this._toolbars)this._toolbars[e]instanceof L.DrawToolbar&&this._toolbars[e].setOptions(t)},_toolbarEnabled:function(t){var e=""+L.stamp(t.target);for(var i in this._toolbars)this._toolbars.hasOwnProperty(i)&&i!==e&&this._toolbars[i].disable()}}),L.Map.mergeOptions({drawControlTooltips:!0,drawControl:!1}),L.Map.addInitHook(function(){this.options.drawControl&&(this.drawControl=new L.Control.Draw,this.addControl(this.drawControl))}),L.Toolbar=L.Class.extend({includes:[L.Mixin.Events],initialize:function(t){L.setOptions(this,t),this._modes={},this._actionButtons=[],this._activeMode=null},enabled:function(){return null!==this._activeMode},disable:function(){this.enabled()&&this._activeMode.handler.disable()},addToolbar:function(t){var e,i=L.DomUtil.create("div","leaflet-draw-section"),o=0,a=this._toolbarClass||"",s=this.getModeHandlers(t);for(this._toolbarContainer=L.DomUtil.create("div","leaflet-draw-toolbar leaflet-bar"),this._map=t,e=0;e<s.length;e++)s[e].enabled&&this._initModeHandler(s[e].handler,this._toolbarContainer,o++,a,s[e].title);return o?(this._lastButtonIndex=--o,this._actionsContainer=L.DomUtil.create("ul","leaflet-draw-actions"),i.appendChild(this._toolbarContainer),i.appendChild(this._actionsContainer),i):void 0},removeToolbar:function(){for(var t in this._modes)this._modes.hasOwnProperty(t)&&(this._disposeButton(this._modes[t].button,this._modes[t].handler.enable,this._modes[t].handler),this._modes[t].handler.disable(),this._modes[t].handler.off("enabled",this._handlerActivated,this).off("disabled",this._handlerDeactivated,this));this._modes={};for(var e=0,i=this._actionButtons.length;i>e;e++)this._disposeButton(this._actionButtons[e].button,this._actionButtons[e].callback,this);this._actionButtons=[],this._actionsContainer=null},_initModeHandler:function(t,e,i,o,a){var s=t.type;this._modes[s]={},this._modes[s].handler=t,this._modes[s].button=this._createButton({title:a,className:o+"-"+s,container:e,callback:this._modes[s].handler.enable,context:this._modes[s].handler}),this._modes[s].buttonIndex=i,this._modes[s].handler.on("enabled",this._handlerActivated,this).on("disabled",this._handlerDeactivated,this)},_createButton:function(t){var e=L.DomUtil.create("a",t.className||"",t.container);return e.href="#",t.text&&(e.innerHTML=t.text),t.title&&(e.title=t.title),L.DomEvent.on(e,"click",L.DomEvent.stopPropagation).on(e,"mousedown",L.DomEvent.stopPropagation).on(e,"dblclick",L.DomEvent.stopPropagation).on(e,"click",L.DomEvent.preventDefault).on(e,"click",t.callback,t.context),e},_disposeButton:function(t,e){L.DomEvent.off(t,"click",L.DomEvent.stopPropagation).off(t,"mousedown",L.DomEvent.stopPropagation).off(t,"dblclick",L.DomEvent.stopPropagation).off(t,"click",L.DomEvent.preventDefault).off(t,"click",e)},_handlerActivated:function(t){this.disable(),this._activeMode=this._modes[t.handler],L.DomUtil.addClass(this._activeMode.button,"leaflet-draw-toolbar-button-enabled"),this._showActionsToolbar(),this.fire("enable")},_handlerDeactivated:function(){this._hideActionsToolbar(),L.DomUtil.removeClass(this._activeMode.button,"leaflet-draw-toolbar-button-enabled"),this._activeMode=null,this.fire("disable")},_createActions:function(t){var e,i,o,a,s=this._actionsContainer,r=this.getActions(t),n=r.length;for(i=0,o=this._actionButtons.length;o>i;i++)this._disposeButton(this._actionButtons[i].button,this._actionButtons[i].callback);for(this._actionButtons=[];s.firstChild;)s.removeChild(s.firstChild);for(var l=0;n>l;l++)"enabled"in r[l]&&!r[l].enabled||(e=L.DomUtil.create("li","",s),a=this._createButton({title:r[l].title,text:r[l].text,container:e,callback:r[l].callback,context:r[l].context}),this._actionButtons.push({button:a,callback:r[l].callback}))},_showActionsToolbar:function(){var t=this._activeMode.buttonIndex,e=this._lastButtonIndex,i=this._activeMode.button.offsetTop-1;this._createActions(this._activeMode.handler),this._actionsContainer.style.top=i+"px",0===t&&(L.DomUtil.addClass(this._toolbarContainer,"leaflet-draw-toolbar-notop"),L.DomUtil.addClass(this._actionsContainer,"leaflet-draw-actions-top")),t===e&&(L.DomUtil.addClass(this._toolbarContainer,"leaflet-draw-toolbar-nobottom"),L.DomUtil.addClass(this._actionsContainer,"leaflet-draw-actions-bottom")),this._actionsContainer.style.display="block"
},_hideActionsToolbar:function(){this._actionsContainer.style.display="none",L.DomUtil.removeClass(this._toolbarContainer,"leaflet-draw-toolbar-notop"),L.DomUtil.removeClass(this._toolbarContainer,"leaflet-draw-toolbar-nobottom"),L.DomUtil.removeClass(this._actionsContainer,"leaflet-draw-actions-top"),L.DomUtil.removeClass(this._actionsContainer,"leaflet-draw-actions-bottom")}}),L.Tooltip=L.Class.extend({initialize:function(t){this._map=t,this._popupPane=t._panes.popupPane,this._container=t.options.drawControlTooltips?L.DomUtil.create("div","leaflet-draw-tooltip",this._popupPane):null,this._singleLineLabel=!1},dispose:function(){this._container&&(this._popupPane.removeChild(this._container),this._container=null)},updateContent:function(t){return this._container?(t.subtext=t.subtext||"",0!==t.subtext.length||this._singleLineLabel?t.subtext.length>0&&this._singleLineLabel&&(L.DomUtil.removeClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!1):(L.DomUtil.addClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!0),this._container.innerHTML=(t.subtext.length>0?'<span class="leaflet-draw-tooltip-subtext">'+t.subtext+"</span><br />":"")+"<span>"+t.text+"</span>",this):this},updatePosition:function(t){var e=this._map.latLngToLayerPoint(t),i=this._container;return this._container&&(i.style.visibility="inherit",L.DomUtil.setPosition(i,e)),this},showAsError:function(){return this._container&&L.DomUtil.addClass(this._container,"leaflet-error-draw-tooltip"),this},removeError:function(){return this._container&&L.DomUtil.removeClass(this._container,"leaflet-error-draw-tooltip"),this}}),L.DrawToolbar=L.Toolbar.extend({options:{polyline:{},polygon:{},rectangle:{},circle:{},marker:{}},initialize:function(t){for(var e in this.options)this.options.hasOwnProperty(e)&&t[e]&&(t[e]=L.extend({},this.options[e],t[e]));this._toolbarClass="leaflet-draw-draw",L.Toolbar.prototype.initialize.call(this,t)},getModeHandlers:function(t){return[{enabled:this.options.polyline,handler:new L.Draw.Polyline(t,this.options.polyline),title:L.drawLocal.draw.toolbar.buttons.polyline},{enabled:this.options.polygon,handler:new L.Draw.Polygon(t,this.options.polygon),title:L.drawLocal.draw.toolbar.buttons.polygon},{enabled:this.options.rectangle,handler:new L.Draw.Rectangle(t,this.options.rectangle),title:L.drawLocal.draw.toolbar.buttons.rectangle},{enabled:this.options.circle,handler:new L.Draw.Circle(t,this.options.circle),title:L.drawLocal.draw.toolbar.buttons.circle},{enabled:this.options.marker,handler:new L.Draw.Marker(t,this.options.marker),title:L.drawLocal.draw.toolbar.buttons.marker}]},getActions:function(t){return[{enabled:t.deleteLastVertex,title:L.drawLocal.draw.toolbar.undo.title,text:L.drawLocal.draw.toolbar.undo.text,callback:t.deleteLastVertex,context:t},{title:L.drawLocal.draw.toolbar.actions.title,text:L.drawLocal.draw.toolbar.actions.text,callback:this.disable,context:this}]},setOptions:function(t){L.setOptions(this,t);for(var e in this._modes)this._modes.hasOwnProperty(e)&&t.hasOwnProperty(e)&&this._modes[e].handler.setOptions(t[e])}}),L.EditToolbar=L.Toolbar.extend({options:{edit:{selectedPathOptions:{color:"#fe57a1",opacity:.6,dashArray:"10, 10",fill:!0,fillColor:"#fe57a1",fillOpacity:.1}},remove:{},featureGroup:null},initialize:function(t){t.edit&&("undefined"==typeof t.edit.selectedPathOptions&&(t.edit.selectedPathOptions=this.options.edit.selectedPathOptions),t.edit=L.extend({},this.options.edit,t.edit)),t.remove&&(t.remove=L.extend({},this.options.remove,t.remove)),this._toolbarClass="leaflet-draw-edit",L.Toolbar.prototype.initialize.call(this,t),this._selectedFeatureCount=0},getModeHandlers:function(t){var e=this.options.featureGroup;return[{enabled:this.options.edit,handler:new L.EditToolbar.Edit(t,{featureGroup:e,selectedPathOptions:this.options.edit.selectedPathOptions}),title:L.drawLocal.edit.toolbar.buttons.edit},{enabled:this.options.remove,handler:new L.EditToolbar.Delete(t,{featureGroup:e}),title:L.drawLocal.edit.toolbar.buttons.remove}]},getActions:function(){return[{title:L.drawLocal.edit.toolbar.actions.save.title,text:L.drawLocal.edit.toolbar.actions.save.text,callback:this._save,context:this},{title:L.drawLocal.edit.toolbar.actions.cancel.title,text:L.drawLocal.edit.toolbar.actions.cancel.text,callback:this.disable,context:this}]},addToolbar:function(t){var e=L.Toolbar.prototype.addToolbar.call(this,t);return this._checkDisabled(),this.options.featureGroup.on("layeradd layerremove",this._checkDisabled,this),e},removeToolbar:function(){this.options.featureGroup.off("layeradd layerremove",this._checkDisabled,this),L.Toolbar.prototype.removeToolbar.call(this)},disable:function(){this.enabled()&&(this._activeMode.handler.revertLayers(),L.Toolbar.prototype.disable.call(this))},_save:function(){this._activeMode.handler.save(),this._activeMode.handler.disable()},_checkDisabled:function(){var t,e=this.options.featureGroup,i=0!==e.getLayers().length;this.options.edit&&(t=this._modes[L.EditToolbar.Edit.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.edit:L.drawLocal.edit.toolbar.buttons.editDisabled)),this.options.remove&&(t=this._modes[L.EditToolbar.Delete.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.remove:L.drawLocal.edit.toolbar.buttons.removeDisabled))}}),L.EditToolbar.Edit=L.Handler.extend({statics:{TYPE:"edit"},includes:L.Mixin.Events,initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),this._selectedPathOptions=e.selectedPathOptions,this._featureGroup=e.featureGroup,!(this._featureGroup instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this._uneditedLayerProps={},this.type=L.EditToolbar.Edit.TYPE},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire("draw:editstart",{handler:this.type}),L.Handler.prototype.enable.call(this),this._featureGroup.on("layeradd",this._enableLayerEdit,this).on("layerremove",this._disableLayerEdit,this))},disable:function(){this._enabled&&(this._featureGroup.off("layeradd",this._enableLayerEdit,this).off("layerremove",this._disableLayerEdit,this),L.Handler.prototype.disable.call(this),this._map.fire("draw:editstop",{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._featureGroup.eachLayer(this._enableLayerEdit,this),this._tooltip=new L.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){this._map&&(this._featureGroup.eachLayer(this._disableLayerEdit,this),this._uneditedLayerProps={},this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this))},revertLayers:function(){this._featureGroup.eachLayer(function(t){this._revertLayer(t)},this)},save:function(){var t=new L.LayerGroup;this._featureGroup.eachLayer(function(e){e.edited&&(t.addLayer(e),e.edited=!1)}),this._map.fire("draw:edited",{layers:t})},_backupLayer:function(t){var e=L.Util.stamp(t);this._uneditedLayerProps[e]||(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?this._uneditedLayerProps[e]={latlngs:L.LatLngUtil.cloneLatLngs(t.getLatLngs())}:t instanceof L.Circle?this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng()),radius:t.getRadius()}:t instanceof L.Marker&&(this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng())}))},_revertLayer:function(t){var e=L.Util.stamp(t);t.edited=!1,this._uneditedLayerProps.hasOwnProperty(e)&&(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?t.setLatLngs(this._uneditedLayerProps[e].latlngs):t instanceof L.Circle?(t.setLatLng(this._uneditedLayerProps[e].latlng),t.setRadius(this._uneditedLayerProps[e].radius)):t instanceof L.Marker&&t.setLatLng(this._uneditedLayerProps[e].latlng))},_toggleMarkerHighlight:function(t){if(t._icon){var e=t._icon;e.style.display="none",L.DomUtil.hasClass(e,"leaflet-edit-marker-selected")?(L.DomUtil.removeClass(e,"leaflet-edit-marker-selected"),this._offsetMarker(e,-4)):(L.DomUtil.addClass(e,"leaflet-edit-marker-selected"),this._offsetMarker(e,4)),e.style.display=""}},_offsetMarker:function(t,e){var i=parseInt(t.style.marginTop,10)-e,o=parseInt(t.style.marginLeft,10)-e;t.style.marginTop=i+"px",t.style.marginLeft=o+"px"},_enableLayerEdit:function(t){var e,i=t.layer||t.target||t,o=i instanceof L.Marker;(!o||i._icon)&&(this._backupLayer(i),this._selectedPathOptions&&(e=L.Util.extend({},this._selectedPathOptions),o?this._toggleMarkerHighlight(i):(i.options.previousOptions=L.Util.extend({dashArray:null},i.options),i instanceof L.Circle||i instanceof L.Polygon||i instanceof L.Rectangle||(e.fill=!1),i.setStyle(e))),o?(i.dragging.enable(),i.on("dragend",this._onMarkerDragEnd)):i.editing.enable())},_disableLayerEdit:function(t){var e=t.layer||t.target||t;e.edited=!1,this._selectedPathOptions&&(e instanceof L.Marker?this._toggleMarkerHighlight(e):(e.setStyle(e.options.previousOptions),delete e.options.previousOptions)),e instanceof L.Marker?(e.dragging.disable(),e.off("dragend",this._onMarkerDragEnd,this)):e.editing.disable()},_onMarkerDragEnd:function(t){var e=t.target;e.edited=!0},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_hasAvailableLayers:function(){return 0!==this._featureGroup.getLayers().length}}),L.EditToolbar.Delete=L.Handler.extend({statics:{TYPE:"remove"},includes:L.Mixin.Events,initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.Util.setOptions(this,e),this._deletableLayers=this.options.featureGroup,!(this._deletableLayers instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this.type=L.EditToolbar.Delete.TYPE},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire("draw:deletestart",{handler:this.type}),L.Handler.prototype.enable.call(this),this._deletableLayers.on("layeradd",this._enableLayerDelete,this).on("layerremove",this._disableLayerDelete,this))},disable:function(){this._enabled&&(this._deletableLayers.off("layeradd",this._enableLayerDelete,this).off("layerremove",this._disableLayerDelete,this),L.Handler.prototype.disable.call(this),this._map.fire("draw:deletestop",{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._deletableLayers.eachLayer(this._enableLayerDelete,this),this._deletedLayers=new L.layerGroup,this._tooltip=new L.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.remove.tooltip.text}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){this._map&&(this._deletableLayers.eachLayer(this._disableLayerDelete,this),this._deletedLayers=null,this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this))},revertLayers:function(){this._deletedLayers.eachLayer(function(t){this._deletableLayers.addLayer(t)},this)},save:function(){this._map.fire("draw:deleted",{layers:this._deletedLayers})},_enableLayerDelete:function(t){var e=t.layer||t.target||t;e.on("click",this._removeLayer,this)},_disableLayerDelete:function(t){var e=t.layer||t.target||t;e.off("click",this._removeLayer,this),this._deletedLayers.removeLayer(e)},_removeLayer:function(t){var e=t.layer||t.target||t;this._deletableLayers.removeLayer(e),this._deletedLayers.addLayer(e)},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_hasAvailableLayers:function(){return 0!==this._deletableLayers.getLayers().length}})}(window,document);
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -174,9 +174,20 @@
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs2.css') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
{% if config.MAPBOX_MAP_ID %}
<script>
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
</script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js') }}"></script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form.js') }}"></script>
{% endmacro %}
......@@ -167,9 +167,20 @@
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs3.css') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
{% if config.MAPBOX_MAP_ID %}
<script>
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
</script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js') }}"></script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form.js') }}"></script>
{% endmacro %}
from flask import Flask
from flask.ext.admin import Admin
from flask.ext.sqlalchemy import SQLAlchemy
def setup():
app = Flask(__name__)
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/flask_admin_test'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
admin = Admin(app)
return app, db, admin
from __future__ import unicode_literals
from nose.tools import eq_, ok_
from flask.ext.admin.contrib.geoa import ModelView
from flask.ext.admin.contrib.geoa.sqltypes import Geometry
from flask.ext.admin.contrib.geoa.fields import GeoJSONField
from . import setup
def create_models(db):
class GeoModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
point = db.Column(Geometry("POINT"))
line = db.Column(Geometry("LINESTRING"))
polygon = db.Column(Geometry("POLYGON"))
multi = db.Column(Geometry("MULTIPOINT"))
def __unicode__(self):
return self.name
db.create_all()
return GeoModel
def test_model():
app, db, admin = setup()
GeoModel = create_models(db)
db.create_all()
view = ModelView(GeoModel, db.session)
admin.add_view(view)
eq_(view.model, GeoModel)
eq_(view._primary_key, 'id')
# Verify form
eq_(view._create_form_class.point.field_class, GeoJSONField)
eq_(view._create_form_class.point.kwargs['geometry_type'], "POINT")
eq_(view._create_form_class.line.field_class, GeoJSONField)
eq_(view._create_form_class.line.kwargs['geometry_type'], "LINESTRING")
eq_(view._create_form_class.polygon.field_class, GeoJSONField)
eq_(view._create_form_class.polygon.kwargs['geometry_type'], "POLYGON")
eq_(view._create_form_class.multi.field_class, GeoJSONField)
eq_(view._create_form_class.multi.kwargs['geometry_type'], "MULTIPOINT")
# Make some test clients
client = app.test_client()
rv = client.get('/admin/geomodel/')
eq_(rv.status_code, 200)
rv = client.get('/admin/geomodel/new/')
eq_(rv.status_code, 200)
rv = client.post('/admin/geomodel/new/', data={
"name": "test1",
"point": '{"type": "Point", "coordinates": [125.8, 10.0]}',
"line": '{"type": "LineString", "coordinates": [[50.2345, 94.2], [50.21, 94.87]]}',
"polygon": '{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}',
"multi": '{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}',
})
eq_(rv.status_code, 302)
model = db.session.query(GeoModel).first()
eq_(model.name, "test1")
eq_(model.point.geom_type, "Point")
eq_(list(model.point.coords), [(125.8, 10.0)])
eq_(model.line.geom_type, "LineString")
eq_(list(model.line.coords), [(50.2345, 94.2), (50.21, 94.87)])
eq_(model.polygon.geom_type, "Polygon")
eq_(list(model.polygon.exterior.coords),
[(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)])
eq_(model.multi.geom_type, "MultiPoint")
eq_(len(model.multi.geoms), 2)
eq_(list(model.multi.geoms[0].coords), [(100.0, 0.0)])
eq_(list(model.multi.geoms[1].coords), [(101.0, 1.0)])
rv = client.get('/admin/geomodel/')
eq_(rv.status_code, 200)
point_opt_1 = '>{"type": "Point", "coordinates": [125.8, 10.0]}</textarea>'
point_opt_2 = '>{"coordinates": [125.8, 10.0], "type": "Point"}</textarea>'
html = rv.data.decode('utf-8')
ok_(point_opt_1 in html or point_opt_2 in html, html)
url = '/admin/geomodel/edit/?id=%s' % model.id
rv = client.get(url)
eq_(rv.status_code, 200)
rv = client.post(url, data={
"name": "edited",
"point": '{"type": "Point", "coordinates": [99.9, 10.5]}',
"line": '', # set to NULL in the database
})
eq_(rv.status_code, 302)
model = db.session.query(GeoModel).first()
eq_(model.name, "edited")
eq_(model.point.geom_type, "Point")
eq_(list(model.point.coords), [(99.9, 10.5)])
eq_(model.line, None)
eq_(model.polygon.geom_type, "Polygon")
eq_(list(model.polygon.exterior.coords),
[(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)])
eq_(model.multi.geom_type, "MultiPoint")
eq_(len(model.multi.geoms), 2)
eq_(list(model.multi.geoms[0].coords), [(100.0, 0.0)])
eq_(list(model.multi.geoms[1].coords), [(101.0, 1.0)])
url = '/admin/geomodel/delete/?id=%s' % model.id
rv = client.post(url)
eq_(rv.status_code, 302)
eq_(db.session.query(GeoModel).count(), 0)
......@@ -56,7 +56,10 @@ setup(
'sqlalchemy',
'flask-mongoengine',
'flask-sqlalchemy',
'flask-babelex'
'flask-babelex',
'shapely',
'geoalchemy2',
'psycopg2',
],
classifiers=[
'Development Status :: 4 - Beta',
......
Flask>=0.7
wtforms>=2.0
Flask-SQLAlchemy>=0.15
peewee
wtf-peewee
flask-mongoengine
pillow
flask-babelex
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment