├── .coveragerc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── README.rst ├── djgeojson ├── __init__.py ├── fields.py ├── http.py ├── models.py ├── nogeos.py ├── serializers.py ├── templatetags │ ├── __init__.py │ └── geojson_tags.py ├── tests.py └── views.py ├── docs ├── Makefile ├── advanced.rst ├── conf.py ├── index.rst ├── installation.rst ├── make.bat ├── models.rst └── views.rst ├── quicktest.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = djgeojson 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests_python: 7 | name: Test on Python ${{ matrix.python_version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python_version: 12 | - '3.9' 13 | - '3.10' 14 | - '3.11' 15 | - '3.12' 16 | - '3.13' 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Python ${{ matrix.python_version }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python_version }} 27 | 28 | - name: Cache pip 29 | uses: actions/cache@v4 30 | with: 31 | # This path is specific to Ubuntu 32 | path: ~/.cache/pip 33 | key: ${{ runner.os }}-pip-${{ matrix.python_version }} 34 | 35 | - name: Install dependencies 36 | run: | 37 | sudo apt-get update -qq 38 | sudo apt-get install -y libproj-dev libgeos-dev gdal-bin libgdal-dev libsqlite3-mod-spatialite 39 | python -m pip install --upgrade pip 40 | python -m pip install --upgrade 'tox>=4.0.0rc3' 41 | 42 | - name: Run tox targets for ${{ matrix.python_version }} 43 | run: tox run -f py$(echo ${{ matrix.python_version }} | tr -d .) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/django,intellij+iml,python 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | media 12 | 13 | ### Intellij+iml ### 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | 17 | # User-specific stuff: 18 | .idea/**/workspace.xml 19 | .idea/**/tasks.xml 20 | 21 | # Sensitive or high-churn files: 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.xml 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | 30 | # Gradle: 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Mongo Explorer plugin: 35 | .idea/**/mongoSettings.xml 36 | 37 | ## File-based project format: 38 | *.iws 39 | 40 | ## Plugin-specific files: 41 | 42 | # IntelliJ 43 | /out/ 44 | 45 | # mpeltonen/sbt-idea plugin 46 | .idea_modules/ 47 | 48 | # JIRA plugin 49 | atlassian-ide-plugin.xml 50 | 51 | # Crashlytics plugin (for Android Studio and IntelliJ) 52 | com_crashlytics_export_strings.xml 53 | crashlytics.properties 54 | crashlytics-build.properties 55 | fabric.properties 56 | 57 | ### Intellij+iml Patch ### 58 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 59 | 60 | *.iml 61 | modules.xml 62 | .idea/misc.xml 63 | *.ipr 64 | 65 | ### Python ### 66 | # Byte-compiled / optimized / DLL files 67 | *.py[cod] 68 | *$py.class 69 | 70 | # C extensions 71 | *.so 72 | 73 | # Distribution / packaging 74 | .Python 75 | env/ 76 | build/ 77 | develop-eggs/ 78 | dist/ 79 | downloads/ 80 | eggs/ 81 | .eggs/ 82 | lib/ 83 | lib64/ 84 | parts/ 85 | sdist/ 86 | var/ 87 | wheels/ 88 | *.egg-info/ 89 | .installed.cfg 90 | *.egg 91 | 92 | # PyInstaller 93 | # Usually these files are written by a python script from a template 94 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 95 | *.manifest 96 | *.spec 97 | 98 | # Installer logs 99 | pip-log.txt 100 | pip-delete-this-directory.txt 101 | 102 | # Unit test / coverage reports 103 | htmlcov/ 104 | .tox/ 105 | .coverage 106 | .coverage.* 107 | .cache 108 | nosetests.xml 109 | coverage.xml 110 | *,cover 111 | .hypothesis/ 112 | 113 | # Translations 114 | *.mo 115 | 116 | # Django stuff: 117 | 118 | # Flask stuff: 119 | instance/ 120 | .webassets-cache 121 | 122 | # Scrapy stuff: 123 | .scrapy 124 | 125 | # Sphinx documentation 126 | docs/_build/ 127 | 128 | # PyBuilder 129 | target/ 130 | 131 | # Jupyter Notebook 132 | .ipynb_checkpoints 133 | 134 | # pyenv 135 | .python-version 136 | 137 | # celery beat schedule file 138 | celerybeat-schedule 139 | 140 | # dotenv 141 | .env 142 | 143 | # virtualenv 144 | .venv 145 | venv/ 146 | ENV/ 147 | 148 | # Spyder project settings 149 | .spyderproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # End of https://www.gitignore.io/api/django,intellij+iml,python 155 | 156 | /.idea -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | ========= 2 | CHANGELOG 3 | ========= 4 | 5 | 4.2.1 (unreleased) 6 | ================== 7 | 8 | - 9 | 10 | 4.2.0 (2025-10-03) 11 | ================== 12 | 13 | - Drop Python 3.8, Django 4.0, and Django 4.1. 14 | - Support Python 3.13, Django 5.1 and Django 5.2. 15 | 16 | 4.1.0 (2024-05-13) 17 | ================== 18 | 19 | - Drop Python 3.7, Django 3.2. Support Python 3.11 and 3.12, Django 4.2 and 5.0. #123 20 | 21 | 4.0.0 (2022-10-17) 22 | ================== 23 | 24 | - Drop support for Python < 3.7 / Django < 3.2 25 | - Remove jsonfield dependency in favor of django.db.models.JSONField 26 | WARNING : This can break your migration. jsonfield used to create a text column for your json data when running 'makemigrations'. However, django.db.models.JSONField creates a json column using database-backend specific column types (ie jsonb on postgresql). As such your model field that used to be a text column is now a jsonb column. 27 | 28 | 3.2.1 (2022-02-21) 29 | ================== 30 | 31 | - Set default initial value to form field #116 32 | 33 | 3.2.0 (2021-04-15) 34 | ================== 35 | 36 | - Support Django 3.2. 37 | 38 | 3.1.0 (2020-10-27) 39 | ================== 40 | 41 | - Drop support for Python 2 and Django < 2.2 42 | - Fix warnings in Django 3.1 43 | - Use explicit srid in get_queryset #106 44 | - Add support for crs_type using the view options #107 45 | 46 | 3.0.0 (2020-03-02) 47 | =================== 48 | 49 | - Fix with_modelname option after first iteration 50 | - Drop Django 1.11 support 51 | - jsonfield 3.0 support, drop 2.x support 52 | 53 | 2.12.0 (2018-12-05) 54 | =================== 55 | 56 | - Change media type to 'application/geo+json' 57 | - Make with_modelname option available in views 58 | - Drop Django 1.8 support. 59 | - Fixed unnecessary GDAL dependency. (#100, thanks @coredumperror) 60 | 61 | 2.11.0 (2017-12-05) 62 | =================== 63 | 64 | - Django 2.0 support. 65 | - Fix setup.py encoding error. 66 | 67 | 2.10.0 (2017-02-28) 68 | =================== 69 | 70 | - python 3.5 support. 71 | 72 | 73 | 2.9.1 (2016-08-22) 74 | ================== 75 | 76 | ** Bug fixes ** 77 | 78 | - Remove deprecation warnings with Django 1.8 (#78, thanks @AchilleAsh) 79 | 80 | 81 | 2.9.0 (2016-02-08) 82 | ================== 83 | 84 | ** New features ** 85 | 86 | - handle natural keys in views (#74, thanks Achille Ash!) 87 | 88 | ** Bug fixes ** 89 | 90 | - Add Django 1.9 compatibility (#69, thanks Julien Le Sech!) 91 | - Fix imports in view.py to work without GEOS (#62, thanks Kevin Cooper!) 92 | 93 | 94 | 2.8.1 (2015-06-17) 95 | ================== 96 | 97 | ** Bug fixes** 98 | 99 | - Fixed detection of GEOS (thanks Kevin Cooper!) 100 | 101 | 2.8.0 (2015-04-17) 102 | ================== 103 | 104 | ** New features ** 105 | 106 | - Support GeoJSON specification for named crs (thanks Alvin Lindstam) 107 | 108 | ** Bug fixes ** 109 | 110 | - Add python 3.2 compatibility (thanks Nikolay Korotkiy, fixes #55) 111 | - Fix tests on Django >= 1.7 (thanks Manel Clos) 112 | 113 | 114 | 2.7.0 (2015-02-21) 115 | ================== 116 | 117 | ** New features ** 118 | 119 | - Add a with_modelname option to serializer 120 | 121 | ** Bug fixes ** 122 | 123 | - change 'fields' to 'properties' in code example 124 | - Adds a warning for "Module version, as defined in PEP-0396 125 | 126 | 127 | 2.6.0 (2014-07-21) 128 | ================== 129 | 130 | ** New features ** 131 | 132 | - Django GeoJSON fields without libgeos installed (thanks Florent Lebreton) 133 | - Properties can be a tuple (fixes #34) 134 | 135 | 136 | 2.5.0 (2014-06-03) 137 | ================== 138 | 139 | ** New features ** 140 | 141 | - Add vector tiles view 142 | - Improved `geojsonfeature` template tag (fixes #15, #16) 143 | - Add various GeoJSON fields, for each geometry type 144 | 145 | ** Bug fixes ** 146 | 147 | - Fix (de)serializers not being usable from command-line (fixes #28) 148 | - Fix import attempt for *django-leaflet* (fixes #27), by Seyi Ogunyemi 149 | - Fix missed SRID after copying a geometry in ``_handle_geom``, by Biel Frontera 150 | 151 | ** Internal changes ** 152 | 153 | - Specify django-leaflet minimal version for GeoJSON model field 154 | - Got rid of shapely for deserialization 155 | 156 | 157 | 2.4.0 (2014-03-22) 158 | ================== 159 | 160 | - Add GeoJSON fields 161 | 162 | 163 | 2.3.0 (2014-02-08) 164 | ================== 165 | 166 | - Python 3 support (thanks @amarandon) 167 | - Add bbox at feature level (thanks @7wonders) 168 | 169 | 170 | 2.2.0 (2013-12-18) 171 | ================== 172 | 173 | - Deserialization: add ability to specify model name as option (thanks @Vross) 174 | - Deserialization: look-up ``ìd`` value in properties whe missing at feature level: (thanks @Vross) 175 | 176 | 177 | 2.1.1 (2013-08-21) 178 | ================== 179 | 180 | - Set default SRID to 4326 in generic GeoJSON views. 181 | 182 | 183 | 2.1.0 (2013-08-19) 184 | ================== 185 | 186 | - Serialize reversed relations using natural keys (fixes #8) 187 | - Support empty geometries (None or NULL in Db) 188 | - Fix serializing property in upper class 189 | 190 | 2.0.1 (2013-07-10) 191 | ================== 192 | 193 | - Fix usage of simplify. 194 | - Expose ``force2d`` option in view 195 | - Allow to have ``pk`` or ``id`` in properties if explicitly listed 196 | 197 | 2.0.0 (2013-07-09) 198 | ================== 199 | 200 | - Complete rewrite using @jeffkistler and @glenrobertson code 201 | - CRS is added to GeoJSON ouput by default 202 | - Ability to build ``pk`` dynamically by passing a lambda 203 | - Ability to provide a ``bbox`` 204 | - Ability to use Django natural keys 205 | - Support of *ValuesQuerySet* 206 | - Support of *ForeignKey* and *ManyToMany* 207 | - Added ``force2d`` option 208 | 209 | ** Backwards incompatible changes ** 210 | 211 | - Geom field is not guessed automatically : Use ``geometry_field`` option, default is ``'geom'``. 212 | - no more ``pk`` in properties : use feature ``id``, or list ``pk`` in properties explicitly. 213 | - ``fields`` list in ``GeoJSONLayer`` was renamed ``properties``. 214 | 215 | 1.2.0 (2013-05-22) 216 | ================== 217 | 218 | - Shapely is now optional (used for deserialization only) 219 | - Add Django to requirements 220 | - Skip a step in GeoJSON conversion of geometries (fixes #6) 221 | 222 | 223 | 1.1.0 (2013-03-06) 224 | ================== 225 | 226 | - Django 1.5 support 227 | - Fix template tag geojsonfeature on empty geometries 228 | 229 | 1.0.0 (2012-08-03) 230 | ================== 231 | 232 | - Initial working version. 233 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGES LICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | django-geojson 3 | ============== 4 | 5 | See the `documentation `_ for more information. 6 | 7 | *django-geojson* is a set of tools to manipulate GeoJSON with Django >= 3.2: 8 | 9 | * (De)Serializer for (Geo)Django objects, querysets and lists 10 | * Base views to serve GeoJSON map layers from models 11 | * GeoJSON model and form fields to avoid spatial database backends 12 | (compatible with *django-leaflet* for map widgets) 13 | 14 | 15 | .. image:: https://readthedocs.org/projects/django-geojson/badge/?version=latest 16 | :target: http://django-geojson.readthedocs.io/en/latest/?badge=latest 17 | :alt: Documentation Status 18 | 19 | .. image:: https://img.shields.io/pypi/v/django-geojson.svg 20 | :target: https://pypi.python.org/pypi/django-geojson 21 | 22 | .. image:: https://img.shields.io/pypi/dm/django-geojson.svg 23 | :target: https://pypi.python.org/pypi/django-geojson 24 | 25 | .. image:: https://travis-ci.org/makinacorpus/django-geojson.png?branch=master 26 | :target: https://travis-ci.org/makinacorpus/django-geojson 27 | 28 | .. image:: https://coveralls.io/repos/makinacorpus/django-geojson/badge.png?branch=master 29 | :target: https://coveralls.io/r/makinacorpus/django-geojson 30 | 31 | 32 | ======= 33 | AUTHORS 34 | ======= 35 | 36 | * Mathieu Leplatre 37 | * Glen Robertson author of django-geojson-tiles at: https://github.com/glenrobertson/django-geojson-tiles/ 38 | * @jeffkistler's author of geojson serializer at: https://gist.github.com/967274 39 | * Ben Welsh and Lukasz Dziedzia for `quick test script `_ 40 | * Florent Lebreton http://github.com/fle 41 | * Julien Le Sech http://www.idreammicro.com 42 | * Kevin Cooper @kevcooper 43 | * Achille Ash @AchilleAsh 44 | * Yann Fouillat (alias Gagaro) 45 | 46 | Version 1.X: 47 | 48 | * Daniel Sokolowski, serializer snippet 49 | * ozzmo, python 2.6 compatibility 50 | 51 | |makinacom|_ 52 | 53 | .. |makinacom| image:: http://depot.makina-corpus.org/public/logo.gif 54 | .. _makinacom: http://www.makina-corpus.com 55 | 56 | ======= 57 | LICENSE 58 | ======= 59 | 60 | Lesser GNU Public License 61 | -------------------------------------------------------------------------------- /djgeojson/__init__.py: -------------------------------------------------------------------------------- 1 | GEOJSON_DEFAULT_SRID = 4326 2 | -------------------------------------------------------------------------------- /djgeojson/fields.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.db.models import JSONField 3 | from django.forms.fields import InvalidJSONInput 4 | from django.forms.fields import JSONField as JSONFormField 5 | from django.forms.widgets import HiddenInput 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | try: 9 | from leaflet.forms.widgets import LeafletWidget 10 | HAS_LEAFLET = True 11 | except ImportError: 12 | HAS_LEAFLET = False 13 | 14 | 15 | class GeoJSONValidator(object): 16 | def __init__(self, geom_type): 17 | self.geom_type = geom_type 18 | 19 | def __call__(self, value): 20 | err_msg = None 21 | geom_type = value.get('type') or '' 22 | if self.geom_type == 'GEOMETRY': 23 | is_geometry = geom_type in ( 24 | "Point", "MultiPoint", "LineString", "MultiLineString", 25 | "Polygon", "MultiPolygon", "GeometryCollection" 26 | ) 27 | if not is_geometry: 28 | err_msg = '%s is not a valid GeoJSON geometry type' % geom_type 29 | else: 30 | if self.geom_type.lower() != geom_type.lower(): 31 | err_msg = '%s does not match geometry type' % geom_type 32 | 33 | if err_msg: 34 | raise ValidationError(err_msg) 35 | 36 | 37 | class GeoJSONFormField(JSONFormField): 38 | widget = LeafletWidget if HAS_LEAFLET else HiddenInput 39 | 40 | def __init__(self, *args, **kwargs): 41 | if not HAS_LEAFLET: 42 | import warnings 43 | warnings.warn('`django-leaflet` is not available.') 44 | geom_type = kwargs.pop('geom_type') 45 | kwargs.setdefault('validators', [GeoJSONValidator(geom_type)]) 46 | kwargs.setdefault('initial', InvalidJSONInput('')) 47 | super(GeoJSONFormField, self).__init__(*args, **kwargs) 48 | 49 | 50 | class GeoJSONField(JSONField): 51 | description = _("Geometry as GeoJSON") 52 | form_class = GeoJSONFormField 53 | dim = 2 54 | geom_type = 'GEOMETRY' 55 | 56 | def formfield(self, **kwargs): 57 | kwargs.setdefault('geom_type', self.geom_type) 58 | kwargs.setdefault('form_class', self.form_class) 59 | return super(GeoJSONField, self).formfield(**kwargs) 60 | 61 | 62 | class GeometryField(GeoJSONField): 63 | pass 64 | 65 | 66 | class GeometryCollectionField(GeometryField): 67 | geom_type = 'GEOMETRYCOLLECTION' 68 | 69 | 70 | class PointField(GeometryField): 71 | geom_type = 'POINT' 72 | 73 | 74 | class MultiPointField(GeometryField): 75 | geom_type = 'MULTIPOINT' 76 | 77 | 78 | class LineStringField(GeometryField): 79 | geom_type = 'LINESTRING' 80 | 81 | 82 | class MultiLineStringField(GeometryField): 83 | geom_type = 'MULTILINESTRING' 84 | 85 | 86 | class PolygonField(GeometryField): 87 | geom_type = 'POLYGON' 88 | 89 | 90 | class MultiPolygonField(GeoJSONField): 91 | geom_type = 'MULTIPOLYGON' 92 | -------------------------------------------------------------------------------- /djgeojson/http.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from django.http import HttpResponse 4 | 5 | 6 | class HttpGeoJSONResponse(HttpResponse): 7 | def __init__(self, **kwargs): 8 | kwargs['content_type'] = 'application/geo+json' 9 | super(HttpGeoJSONResponse, self).__init__(**kwargs) 10 | 11 | 12 | class HttpJSONResponse(HttpGeoJSONResponse): 13 | def __init__(self, **kwargs): 14 | warnings.warn("The 'HttpJSONResponse' class was renamed to 'HttpGeoJSONResponse'", 15 | DeprecationWarning) 16 | super(HttpJSONResponse, self).__init__(**kwargs) 17 | -------------------------------------------------------------------------------- /djgeojson/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/django-geojson/9134370e43d30f78baf44fa0c847ffbe7ae3db33/djgeojson/models.py -------------------------------------------------------------------------------- /djgeojson/nogeos.py: -------------------------------------------------------------------------------- 1 | class GEOSGeometry(object): 2 | """ 3 | A simple mock of django.contrib.gis.geos.GEOSGeometry to make 4 | django-geojson work without libgeos 5 | """ 6 | def __init__(self, geo_input, srid=None): 7 | self.srid = srid 8 | self.geojson = geo_input 9 | return 10 | 11 | 12 | class Polygon(GEOSGeometry): 13 | pass 14 | 15 | 16 | class WKBWriter(object): 17 | pass 18 | -------------------------------------------------------------------------------- /djgeojson/serializers.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This code mainly comes from @glenrobertson's django-geoson-tiles at: 4 | https://github.com/glenrobertson/django-geojson-tiles/ 5 | 6 | Itself, adapted from @jeffkistler's geojson serializer at: https://gist.github.com/967274 7 | """ 8 | import json 9 | import logging 10 | from contextlib import contextmanager 11 | from io import StringIO # NOQA 12 | 13 | import django 14 | from django.db.models.base import Model 15 | 16 | try: 17 | from django.db.models.query import QuerySet, ValuesQuerySet 18 | except ImportError: 19 | from django.db.models.query import QuerySet 20 | ValuesQuerySet = None 21 | 22 | from django.core.exceptions import ImproperlyConfigured 23 | from django.core.serializers.base import ( 24 | DeserializationError, 25 | SerializationError, 26 | ) 27 | from django.core.serializers.json import DjangoJSONEncoder 28 | from django.core.serializers.python import Deserializer as PythonDeserializer 29 | from django.core.serializers.python import Serializer as PythonSerializer 30 | from django.forms.models import model_to_dict 31 | from django.utils.encoding import smart_str 32 | 33 | try: 34 | from django.contrib.gis.db.models.fields import GeometryField 35 | from django.contrib.gis.geos import GEOSGeometry, WKBWriter 36 | except (ImportError, ImproperlyConfigured): 37 | from .nogeos import WKBWriter 38 | from .nogeos import GEOSGeometry 39 | from .fields import GeometryField 40 | 41 | from . import GEOJSON_DEFAULT_SRID 42 | from .fields import GeoJSONField 43 | 44 | if django.VERSION >= (5, 2): 45 | get_model = PythonDeserializer._get_model_from_node 46 | else: 47 | from django.core.serializers.python import _get_model as get_model 48 | 49 | logger = logging.getLogger(__name__) 50 | 51 | 52 | def hasattr_lazy(obj, name): 53 | if isinstance(obj, dict): 54 | return name in obj 55 | return name in dir(obj) 56 | 57 | 58 | def get_field_remote_field(field): 59 | """ For Django 1.8/2.0 compatibility """ 60 | if django.VERSION < (1, 9): 61 | return field.rel 62 | else: 63 | return field.remote_field 64 | 65 | 66 | class DjangoGeoJSONEncoder(DjangoJSONEncoder): 67 | 68 | def default(self, o): 69 | if isinstance(o, GEOSGeometry): 70 | return json.loads(o.geojson) 71 | else: 72 | return super(DjangoGeoJSONEncoder, self).default(o) 73 | 74 | 75 | @contextmanager 76 | def json_encoder_with_precision(precision, JSONEncoderClass): 77 | """ 78 | Context manager to set float precision during json encoding 79 | """ 80 | needs_class_hack = not hasattr(json.encoder, 'FLOAT_REPR') 81 | try: 82 | if precision is not None: 83 | def float_repr(o): 84 | return format(o, '.%sf' % precision) 85 | if not needs_class_hack: 86 | # python is not 3.5 87 | original_float_repr = json.encoder.FLOAT_REPR 88 | json.encoder.FLOAT_REPR = float_repr 89 | else: 90 | # HACK to allow usage of float precision in python 3.5 91 | # Needed because python 3.5 removes the global FLOAT_REPR hack 92 | 93 | class JSONEncoderClass(JSONEncoderClass): 94 | FLOAT_REPR = float.__repr__ 95 | 96 | # Follows code copied from cPython Lib/json/encoder.py 97 | # The only change is that in floatstr _repr=float.__repr__ is replaced 98 | 99 | def iterencode(self, o, _one_shot=False): 100 | """Encode the given object and yield each string 101 | representation as available. 102 | 103 | For example:: 104 | 105 | for chunk in JSONEncoder().iterencode(bigobject): 106 | mysocket.write(chunk) 107 | 108 | """ 109 | if self.check_circular: 110 | markers = {} 111 | else: 112 | markers = None 113 | if self.ensure_ascii: 114 | _encoder = json.encoder.encode_basestring_ascii 115 | else: 116 | _encoder = json.encoder.encode_basestring 117 | 118 | def floatstr(o, allow_nan=self.allow_nan, _repr=float_repr, _inf=json.encoder.INFINITY, _neginf=-json.encoder.INFINITY): 119 | # Check for specials. Note that this type of test is processor 120 | # and/or platform-specific, so do tests which don't depend on the 121 | # internals. 122 | 123 | if o != o: 124 | text = 'NaN' 125 | elif o == _inf: 126 | text = 'Infinity' 127 | elif o == _neginf: 128 | text = '-Infinity' 129 | else: 130 | return _repr(o) 131 | 132 | if not allow_nan: 133 | raise ValueError( 134 | "Out of range float values are not JSON compliant: " + 135 | repr(o)) 136 | 137 | return text 138 | 139 | if (_one_shot and json.encoder.c_make_encoder is not None and self.indent is None): 140 | _iterencode = json.encoder.c_make_encoder( 141 | markers, self.default, _encoder, self.indent, 142 | self.key_separator, self.item_separator, self.sort_keys, 143 | self.skipkeys, self.allow_nan) 144 | else: 145 | _iterencode = json.encoder._make_iterencode( 146 | markers, self.default, _encoder, self.indent, floatstr, 147 | self.key_separator, self.item_separator, self.sort_keys, 148 | self.skipkeys, _one_shot) 149 | return _iterencode(o, 0) 150 | yield JSONEncoderClass 151 | finally: 152 | if precision is not None: 153 | if not needs_class_hack: 154 | # restore FLOAT_REPR 155 | json.encoder.FLOAT_REPR = original_float_repr 156 | 157 | 158 | class Serializer(PythonSerializer): 159 | 160 | internal_use_only = False 161 | 162 | def start_serialization(self): 163 | self.feature_collection = {"type": "FeatureCollection", "features": []} 164 | if self.crs is not False: 165 | self.feature_collection["crs"] = self.get_crs() 166 | 167 | bbox = self.options.pop('bbox', None) 168 | if bbox: 169 | self.feature_collection["bbox"] = bbox 170 | 171 | self._current = None 172 | 173 | def get_crs(self): 174 | crs = {} 175 | crs_type = self.options.pop('crs_type', None) 176 | properties = {} 177 | if crs_type == "name": 178 | # todo: GeoJSON Spec: OGC CRS URNs such as "urn:ogc:def:crs:OGC:1.3:CRS84" shall be preferred over legacy identifiers such as "EPSG:4326": 179 | properties["name"] = "EPSG:%s" % (str(self.srid)) 180 | else: # preserve default behaviour 181 | crs_type = "link" 182 | properties["href"] = "http://spatialreference.org/ref/epsg/%s/" % (str(self.srid)) 183 | properties["type"] = "proj4" 184 | crs["type"] = crs_type 185 | crs["properties"] = properties 186 | return crs 187 | 188 | def start_object(self, obj): 189 | self._current = {"type": "Feature", "properties": {}} 190 | 191 | # Try to determine the primary key from the obj 192 | # self.primary_key can be a function (callable on obj), or a string 193 | # if self.primary_key is not set, use obj.pk if obj is a Model 194 | # otherwise the primary key will not be used 195 | primary_key = None 196 | if self.primary_key and hasattr(self.primary_key, '__call__'): 197 | primary_key = self.primary_key(obj) 198 | elif self.primary_key and isinstance(self.primary_key, str): 199 | if isinstance(obj, Model): 200 | primary_key = getattr(obj, self.primary_key) 201 | else: 202 | primary_key = obj[self.primary_key] 203 | elif isinstance(obj, Model): 204 | primary_key = obj.pk 205 | 206 | if primary_key: 207 | self._current['id'] = primary_key 208 | 209 | def end_object(self, obj): 210 | # Add extra properties from dynamic attributes 211 | extras = [] 212 | if isinstance(self.properties, dict): 213 | extras = [field for field, name in self.properties.items() 214 | if name not in self._current['properties']] 215 | elif isinstance(self.properties, (list, tuple)): 216 | extras = [field for field in self.properties 217 | if field not in self._current['properties']] 218 | 219 | for field in extras: 220 | if hasattr_lazy(obj, field): 221 | self.handle_field(obj, field) 222 | 223 | # Add extra-info for deserializing 224 | with_modelname = self.options.get('with_modelname', True) 225 | if hasattr(obj, '_meta') and with_modelname: 226 | self._current['properties']['model'] = smart_str(obj._meta) 227 | 228 | # If geometry not in model fields, may be a dynamic attribute 229 | if 'geometry' not in self._current: 230 | if hasattr_lazy(obj, self.geometry_field): 231 | geometry = getattr(obj, self.geometry_field) 232 | self._handle_geom(geometry) 233 | else: 234 | logger.warn("No GeometryField found in object") 235 | 236 | self.feature_collection["features"].append(self._current) 237 | self._current = None 238 | 239 | def end_serialization(self): 240 | self.options.pop('stream', None) 241 | self.options.pop('properties', None) 242 | self.options.pop('primary_key', None) 243 | self.options.pop('geometry_field', None) 244 | self.options.pop('use_natural_keys', None) 245 | self.options.pop('crs', None) 246 | self.options.pop('srid', None) 247 | self.options.pop('force2d', None) 248 | self.options.pop('simplify', None) 249 | self.options.pop('bbox', None) 250 | self.options.pop('bbox_auto', None) 251 | self.options.pop('with_modelname', None) 252 | 253 | # Optional float precision control 254 | precision = self.options.pop('precision', None) 255 | 256 | with json_encoder_with_precision(precision, DjangoGeoJSONEncoder) as cls: 257 | json.dump(self.feature_collection, self.stream, cls=cls, **self.options) 258 | 259 | def _handle_geom(self, value): 260 | """ Geometry processing (in place), depending on options """ 261 | if value is None: 262 | geometry = None 263 | elif isinstance(value, dict) and 'type' in value: 264 | geometry = value 265 | else: 266 | if isinstance(value, GEOSGeometry): 267 | geometry = value 268 | else: 269 | try: 270 | # this will handle string representations (e.g. ewkt, bwkt) 271 | geometry = GEOSGeometry(value) 272 | except ValueError: 273 | # if the geometry couldn't be parsed. 274 | # we can't generate valid geojson 275 | error_msg = 'The field ["%s", "%s"] could not be parsed as a valid geometry' % ( 276 | self.geometry_field, value 277 | ) 278 | raise SerializationError(error_msg) 279 | 280 | # Optional force 2D 281 | if self.options.get('force2d'): 282 | wkb_w = WKBWriter() 283 | wkb_w.outdim = 2 284 | geometry = GEOSGeometry(wkb_w.write(geometry), srid=geometry.srid) 285 | # Optional geometry simplification 286 | simplify = self.options.get('simplify') 287 | if simplify is not None: 288 | geometry = geometry.simplify(tolerance=simplify, preserve_topology=True) 289 | # Optional geometry reprojection 290 | if geometry.srid and geometry.srid != self.srid: 291 | geometry.transform(self.srid) 292 | # Optional bbox 293 | if self.options.get('bbox_auto'): 294 | self._current['bbox'] = geometry.extent 295 | 296 | self._current['geometry'] = geometry 297 | 298 | def handle_field(self, obj, field_name): 299 | if isinstance(obj, Model): 300 | value = getattr(obj, field_name) 301 | elif isinstance(obj, dict): 302 | value = obj[field_name] 303 | else: 304 | # Only supports dicts and models, not lists (e.g. values_list) 305 | return 306 | 307 | if field_name == self.geometry_field: 308 | self._handle_geom(value) 309 | 310 | elif self.properties and field_name in self.properties: 311 | # set the field name to the key's value mapping in self.properties 312 | if isinstance(self.properties, dict): 313 | property_name = self.properties[field_name] 314 | self._current['properties'][property_name] = value 315 | else: 316 | self._current['properties'][field_name] = value 317 | 318 | elif not self.properties: 319 | self._current['properties'][field_name] = value 320 | 321 | def getvalue(self): 322 | if callable(getattr(self.stream, 'getvalue', None)): 323 | return self.stream.getvalue() 324 | 325 | def handle_fk_field(self, obj, field): 326 | related = getattr(obj, field.name) 327 | if related is not None: 328 | if self.use_natural_keys and hasattr(related, 'natural_key'): 329 | related = related.natural_key() 330 | else: 331 | if get_field_remote_field(field).field_name == related._meta.pk.name: 332 | # Related to remote object via primary key 333 | related = related._get_pk_val() 334 | else: 335 | # Related to remote object via other field 336 | related = smart_str(getattr(related, get_field_remote_field(field).field_name), strings_only=True) 337 | self._current['properties'][field.name] = related 338 | 339 | def handle_m2m_field(self, obj, field): 340 | if get_field_remote_field(field).through._meta.auto_created: 341 | remote_field = get_field_remote_field(field) 342 | if django.VERSION < (1, 9): # Django 1.8/2.0 compatibility 343 | remote_model = remote_field.to 344 | else: 345 | remote_model = remote_field.related_model 346 | if self.use_natural_keys and hasattr(remote_model, 'natural_key'): 347 | def m2m_value(value): 348 | return value.natural_key() 349 | else: 350 | def m2m_value(value): 351 | return smart_str(value._get_pk_val(), strings_only=True) 352 | self._current['properties'][field.name] = [m2m_value(related) 353 | for related in getattr(obj, field.name).iterator()] 354 | 355 | def handle_reverse_field(self, obj, field, field_name): 356 | if self.use_natural_keys and hasattr(field.model, 'natural_key'): 357 | def reverse_value(value): 358 | return value.natural_key() 359 | else: 360 | def reverse_value(value): 361 | return smart_str(value._get_pk_val(), strings_only=True) 362 | values = [reverse_value(related) for related in getattr(obj, field_name).iterator()] 363 | self._current['properties'][field_name] = values 364 | 365 | def serialize_object_list(self, objects): 366 | if len(objects) == 0: 367 | return 368 | 369 | # Transform to list of dicts instead of objects 370 | if not isinstance(objects[0], dict): 371 | values = [] 372 | for obj in objects: 373 | objdict = model_to_dict(obj) 374 | # In case geometry is not a DB field 375 | if self.geometry_field not in objdict: 376 | objdict[self.geometry_field] = getattr(obj, self.geometry_field) 377 | if self.properties: 378 | extras = [f for f in self.properties if hasattr(obj, f)] 379 | for field_name in extras: 380 | objdict[field_name] = getattr(obj, field_name) 381 | values.append(objdict) 382 | objects = values 383 | 384 | self.serialize_values_queryset(objects) 385 | 386 | def serialize_values_queryset(self, queryset): 387 | for obj in queryset: 388 | self.start_object(obj) 389 | 390 | # handle the geometry field 391 | self.handle_field(obj, self.geometry_field) 392 | 393 | for field_name in obj: 394 | if field_name not in obj: 395 | continue 396 | if self.properties is None or field_name in self.properties: 397 | self.handle_field(obj, field_name) 398 | 399 | self.end_object(obj) 400 | 401 | def serialize_queryset(self, queryset): 402 | opts = queryset.model._meta 403 | local_fields = opts.local_fields 404 | many_to_many_fields = opts.many_to_many 405 | reversed_fields = [obj.field for obj in get_all_related_objects(opts)] 406 | reversed_fields += [obj.field for obj in get_all_related_many_to_many_objects(opts)] 407 | 408 | # populate each queryset obj as a feature 409 | for obj in queryset: 410 | self.start_object(obj) 411 | 412 | # handle the geometry field 413 | self.handle_field(obj, self.geometry_field) 414 | 415 | # handle the property fields 416 | for field in local_fields: 417 | # don't include the pk in the properties 418 | # as it is in the id of the feature 419 | # except if explicitly listed in properties 420 | if field.name == opts.pk.name and \ 421 | (self.properties is None or 'id' not in self.properties): 422 | continue 423 | # ignore other geometries 424 | if isinstance(field, GeometryField): 425 | continue 426 | 427 | if field.serialize or field.primary_key: 428 | if get_field_remote_field(field) is None: 429 | if self.properties is None or field.attname in self.properties: 430 | self.handle_field(obj, field.name) 431 | else: 432 | if self.properties is None or field.attname[:-3] in self.properties: 433 | self.handle_fk_field(obj, field) 434 | 435 | for field in many_to_many_fields: 436 | if field.serialize: 437 | if self.properties is None or field.attname in self.properties: 438 | self.handle_m2m_field(obj, field) 439 | 440 | for field in reversed_fields: 441 | if field.serialize: 442 | field_name = get_field_remote_field(field).related_name or opts.object_name.lower() 443 | if self.properties is None or field_name in self.properties: 444 | self.handle_reverse_field(obj, field, field_name) 445 | self.end_object(obj) 446 | 447 | def serialize(self, queryset, **options): 448 | """ 449 | Serialize a queryset. 450 | """ 451 | self.options = options 452 | 453 | self.stream = options.get("stream", StringIO()) 454 | self.primary_key = options.get("primary_key", None) 455 | self.properties = options.get("properties") 456 | self.geometry_field = options.get("geometry_field", "geom") 457 | self.use_natural_keys = options.get("use_natural_keys", False) 458 | self.bbox = options.get("bbox", None) 459 | self.bbox_auto = options.get("bbox_auto", None) 460 | self.srid = options.get("srid", GEOJSON_DEFAULT_SRID) 461 | self.crs = options.get("crs", True) 462 | self.crs_type = options.get("crs_type", 'name') 463 | 464 | self.start_serialization() 465 | 466 | if ValuesQuerySet is not None and isinstance(queryset, ValuesQuerySet): 467 | self.serialize_values_queryset(queryset) 468 | 469 | elif isinstance(queryset, list): 470 | self.serialize_object_list(queryset) 471 | 472 | elif isinstance(queryset, QuerySet): 473 | self.serialize_queryset(queryset) 474 | 475 | self.end_serialization() 476 | return self.getvalue() 477 | 478 | 479 | def Deserializer(stream_or_string, **options): 480 | """ 481 | Deserialize a stream or string of JSON data. 482 | """ 483 | 484 | geometry_field = options.get("geometry_field", "geom") 485 | 486 | def FeatureToPython(dictobj): 487 | properties = dictobj['properties'] 488 | model_name = options.get("model_name") or properties.pop('model') 489 | # Deserialize concrete fields only (bypass dynamic properties) 490 | model = get_model(model_name) 491 | field_names = [f.name for f in model._meta.fields] 492 | fields = {} 493 | for k, v in properties.items(): 494 | if k in field_names: 495 | fields[k] = v 496 | obj = { 497 | "model": model_name, 498 | "pk": dictobj.get('id') or properties.get('id'), 499 | "fields": fields 500 | } 501 | if isinstance(model._meta.get_field(geometry_field), GeoJSONField): 502 | obj['fields'][geometry_field] = dictobj['geometry'] 503 | else: 504 | shape = GEOSGeometry(json.dumps(dictobj['geometry'])) 505 | obj['fields'][geometry_field] = shape.wkt 506 | return obj 507 | 508 | if isinstance(stream_or_string, str): 509 | stream = StringIO(stream_or_string) 510 | else: 511 | stream = stream_or_string 512 | try: 513 | collection = json.load(stream) 514 | objects = [FeatureToPython(f) for f in collection['features']] 515 | for obj in PythonDeserializer(objects, **options): 516 | yield obj 517 | except GeneratorExit: 518 | raise 519 | except Exception as e: 520 | # Map to deserializer error 521 | raise DeserializationError(repr(e)) 522 | 523 | 524 | def get_all_related_objects(opts): 525 | """ 526 | Django 1.8 changed meta api, see 527 | https://docs.djangoproject.com/en/1.8/ref/models/meta/#migrating-old-meta-api 528 | https://code.djangoproject.com/ticket/12663 529 | https://github.com/django/django/pull/3848 530 | Initially from Django REST Framework: 531 | https://github.com/tomchristie/django-rest-framework/blob/3.3.2/rest_framework/compat.py 532 | :param opts: Options instance 533 | :return: list of relations except many-to-many ones 534 | """ 535 | if django.VERSION < (1, 8): 536 | return opts.get_all_related_objects() 537 | else: 538 | return [r for r in opts.related_objects if not r.field.many_to_many] 539 | 540 | 541 | def get_all_related_many_to_many_objects(opts): 542 | """ 543 | Django 1.8 changed meta api, see docstr in get_all_related_objects() 544 | 545 | :param opts: Options instance 546 | :return: list of many-to-many relations 547 | """ 548 | if django.VERSION < (1, 8): 549 | return opts.get_all_related_many_to_many_objects() 550 | else: 551 | return [r for r in opts.related_objects if r.field.many_to_many] 552 | -------------------------------------------------------------------------------- /djgeojson/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makinacorpus/django-geojson/9134370e43d30f78baf44fa0c847ffbe7ae3db33/djgeojson/templatetags/__init__.py -------------------------------------------------------------------------------- /djgeojson/templatetags/geojson_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | from django import template 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | try: 8 | from django.contrib.gis.db.models.fields import GeometryField 9 | from django.contrib.gis.geos import GEOSGeometry 10 | except (ImportError, ImproperlyConfigured): 11 | from ..nogeos import GEOSGeometry 12 | from ..fields import GeometryField 13 | 14 | from .. import GEOJSON_DEFAULT_SRID 15 | from ..serializers import DjangoGeoJSONEncoder, Serializer 16 | 17 | register = template.Library() 18 | 19 | 20 | @register.filter 21 | def geojsonfeature(source, params=''): 22 | """ 23 | :params: A string with the following optional tokens: 24 | "properties:field:srid" 25 | """ 26 | parse = re.search(r'(?P((\w+)(,\w+)*)?)(:(?P(\w+)?))?(:(?P(\d+)?))?', params) 27 | if parse: 28 | parse = parse.groupdict() 29 | else: 30 | parse = {} 31 | 32 | geometry_field = parse.get('field') or 'geom' 33 | properties = parse.get('properties', '').split(',') 34 | srid = parse.get('srid') or GEOJSON_DEFAULT_SRID 35 | 36 | if source is None or isinstance(source, str): 37 | return 'null' 38 | 39 | if isinstance(source, (GEOSGeometry, GeometryField)): 40 | encoder = DjangoGeoJSONEncoder() 41 | if source.srid != srid: 42 | source.transform(srid) 43 | feature = {"type": "Feature", "properties": {}} 44 | feature['geometry'] = encoder.default(source) 45 | return json.dumps(feature) 46 | 47 | serializer = Serializer() 48 | 49 | if not hasattr(source, '__iter__'): 50 | source = [source] 51 | 52 | return serializer.serialize(source, properties=properties, 53 | geometry_field=geometry_field, srid=srid) 54 | -------------------------------------------------------------------------------- /djgeojson/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import django 4 | from django.conf import settings 5 | from django.contrib.gis.db import models 6 | from django.contrib.gis.geos import GeometryCollection, LineString, Point 7 | from django.core import serializers 8 | from django.core.exceptions import SuspiciousOperation, ValidationError 9 | from django.forms import HiddenInput 10 | from django.test import TestCase 11 | from django.utils.encoding import smart_str 12 | 13 | from .fields import GeoJSONField, GeoJSONFormField, GeoJSONValidator 14 | from .serializers import Serializer 15 | from .templatetags.geojson_tags import geojsonfeature 16 | from .views import GeoJSONLayerView, TiledGeoJSONLayerView 17 | 18 | settings.SERIALIZATION_MODULES = {'geojson': 'djgeojson.serializers'} 19 | 20 | 21 | class PictureMixin(object): 22 | 23 | @property 24 | def picture(self): 25 | return 'image.png' 26 | 27 | 28 | class Country(models.Model): 29 | label = models.CharField(max_length=20) 30 | geom = models.PolygonField(spatial_index=False, srid=4326) 31 | 32 | if django.VERSION < (1, 9): 33 | objects = models.GeoManager() 34 | 35 | def natural_key(self): 36 | return self.label 37 | 38 | 39 | class Route(PictureMixin, models.Model): 40 | name = models.CharField(max_length=20) 41 | geom = models.LineStringField(spatial_index=False, srid=4326) 42 | countries = models.ManyToManyField(Country) 43 | 44 | def natural_key(self): 45 | return self.name 46 | 47 | @property 48 | def upper_name(self): 49 | return self.name.upper() 50 | 51 | if django.VERSION < (1, 9): 52 | objects = models.GeoManager() 53 | 54 | 55 | class Sign(models.Model): 56 | label = models.CharField(max_length=20) 57 | route = models.ForeignKey(Route, related_name='signs', on_delete=models.PROTECT) 58 | 59 | def natural_key(self): 60 | return self.label 61 | 62 | @property 63 | def geom(self): 64 | return self.route.geom.centroid 65 | 66 | 67 | class GeoJsonDeSerializerTest(TestCase): 68 | 69 | def test_basic(self): 70 | input_geojson = """ 71 | {"type": "FeatureCollection", 72 | "features": [ 73 | { "type": "Feature", 74 | "properties": {"model": "djgeojson.route", "name": "green", "upper_name": "RED"}, 75 | "id": 1, 76 | "geometry": { 77 | "type": "LineString", 78 | "coordinates": [ 79 | [0.0, 0.0], 80 | [1.0, 1.0] 81 | ] 82 | } 83 | }, 84 | { "type": "Feature", 85 | "properties": {"model": "djgeojson.route", "name": "blue"}, 86 | "id": 2, 87 | "geometry": { 88 | "type": "LineString", 89 | "coordinates": [ 90 | [0.0, 0.0], 91 | [1.0, 1.0] 92 | ] 93 | } 94 | } 95 | ]}""" 96 | 97 | # Deserialize into a list of objects 98 | objects = list(serializers.deserialize('geojson', input_geojson)) 99 | 100 | # Were three objects deserialized? 101 | self.assertEqual(len(objects), 2) 102 | 103 | # Did the objects deserialize correctly? 104 | self.assertEqual(objects[1].object.name, "blue") 105 | self.assertEqual(objects[0].object.upper_name, "GREEN") 106 | self.assertEqual(objects[0].object.geom, 107 | LineString((0.0, 0.0), (1.0, 1.0), srid=objects[0].object.geom.srid)) 108 | 109 | def test_with_model_name_passed_as_argument(self): 110 | input_geojson = """ 111 | {"type": "FeatureCollection", 112 | "features": [ 113 | { "type": "Feature", 114 | "properties": {"name": "bleh"}, 115 | "id": 24, 116 | "geometry": { 117 | "type": "LineString", 118 | "coordinates": [ 119 | [1, 2], 120 | [42, 3] 121 | ] 122 | } 123 | } 124 | ]}""" 125 | 126 | my_object = list(serializers.deserialize( 127 | 'geojson', input_geojson, model_name='djgeojson.route'))[0].object 128 | 129 | self.assertEqual(my_object.name, "bleh") 130 | 131 | 132 | class GeoJsonSerializerTest(TestCase): 133 | 134 | def test_basic(self): 135 | # Stuff to serialize 136 | route1 = Route.objects.create( 137 | name='green', geom="LINESTRING (0 0, 1 1)") 138 | route2 = Route.objects.create( 139 | name='blue', geom="LINESTRING (0 0, 1 1)") 140 | route3 = Route.objects.create(name='red', geom="LINESTRING (0 0, 1 1)") 141 | 142 | actual_geojson = json.loads(serializers.serialize( 143 | 'geojson', Route.objects.all(), properties=['name'])) 144 | self.assertEqual( 145 | actual_geojson, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "name": "green"}, "id": route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "name": "blue"}, "id": route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "name": "red"}, "id": route3.pk}]}) 146 | actual_geojson_with_prop = json.loads( 147 | serializers.serialize( 148 | 'geojson', Route.objects.all(), 149 | properties=['name', 'upper_name', 'picture'])) 150 | self.assertEqual(actual_geojson_with_prop, 151 | {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"picture": "image.png", "model": "djgeojson.route", "upper_name": "GREEN", "name": "green"}, "id": route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"picture": "image.png", "model": "djgeojson.route", "upper_name": "BLUE", "name": "blue"}, "id": route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"picture": "image.png", "model": "djgeojson.route", "upper_name": "RED", "name": "red"}, "id": route3.pk}]}) 152 | 153 | def test_precision(self): 154 | serializer = Serializer() 155 | features = json.loads(serializer.serialize( 156 | [{'geom': 'SRID=2154;POINT (1 1)'}], precision=2, crs=False)) 157 | self.assertEqual( 158 | features, {"type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [-1.36, -5.98]}, "type": "Feature", "properties": {}}]}) 159 | 160 | def test_simplify(self): 161 | serializer = Serializer() 162 | features = json.loads(serializer.serialize( 163 | [{'geom': 'SRID=4326;LINESTRING (1 1, 1.5 1, 2 3, 3 3)'}], simplify=0.5, crs=False)) 164 | self.assertEqual( 165 | features, {"type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[1.0, 1.0], [2.0, 3.0], [3.0, 3.0]]}, "type": "Feature", "properties": {}}]}) 166 | 167 | def test_force2d(self): 168 | serializer = Serializer() 169 | features2d = json.loads(serializer.serialize( 170 | [{'geom': 'SRID=4326;POINT Z (1 2 3)'}], 171 | force2d=True, crs=False)) 172 | self.assertEqual( 173 | features2d, {"type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [1.0, 2.0]}, "type": "Feature", "properties": {}}]}) 174 | 175 | def test_named_crs(self): 176 | serializer = Serializer() 177 | features = json.loads(serializer.serialize( 178 | [{'geom': 'SRID=4326;POINT (1 2)'}], 179 | crs_type="name")) 180 | self.assertEqual( 181 | features['crs'], {"type": "name", "properties": {"name": "EPSG:4326"}}) 182 | 183 | def test_misspelled_named_crs(self): 184 | serializer = Serializer() 185 | features = json.loads(serializer.serialize( 186 | [{'geom': 'SRID=4326;POINT (1 2)'}], 187 | crs_type="named")) 188 | self.assertEqual( 189 | features['crs'], {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}) 190 | 191 | def test_pk_property(self): 192 | route = Route.objects.create(name='red', geom="LINESTRING (0 0, 1 1)") 193 | serializer = Serializer() 194 | features2d = json.loads(serializer.serialize( 195 | Route.objects.all(), properties=['id'], crs=False)) 196 | self.assertEqual( 197 | features2d, {"type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "id": route.pk}, "id": route.pk}]}) 198 | 199 | def test_geometry_property(self): 200 | class Basket(models.Model): 201 | 202 | @property 203 | def geom(self): 204 | return GeometryCollection(LineString((3, 4, 5), (6, 7, 8)), Point(1, 2, 3), srid=4326) 205 | 206 | serializer = Serializer() 207 | features = json.loads( 208 | serializer.serialize([Basket()], crs=False, force2d=True)) 209 | expected_content = {"type": "FeatureCollection", "features": [{"geometry": {"type": "GeometryCollection", "geometries": [{"type": "LineString", "coordinates": [[3.0, 4.0], [6.0, 7.0]]}, {"type": "Point", "coordinates": [1.0, 2.0]}]}, "type": "Feature", "properties": {"id": None}}]} 210 | self.assertEqual(features, expected_content) 211 | 212 | def test_none_geometry(self): 213 | class Empty(models.Model): 214 | geom = None 215 | serializer = Serializer() 216 | features = json.loads(serializer.serialize([Empty()], crs=False)) 217 | self.assertEqual( 218 | features, { 219 | "type": "FeatureCollection", 220 | "features": [{ 221 | "geometry": None, 222 | "type": "Feature", 223 | "properties": {"id": None}}] 224 | }) 225 | 226 | def test_bbox_auto(self): 227 | serializer = Serializer() 228 | features = json.loads(serializer.serialize([{'geom': 'SRID=4326;LINESTRING (1 1, 3 3)'}], 229 | bbox_auto=True, crs=False)) 230 | self.assertEqual( 231 | features, { 232 | "type": "FeatureCollection", 233 | "features": [{ 234 | "geometry": {"type": "LineString", "coordinates": [[1.0, 1.0], [3.0, 3.0]]}, 235 | "type": "Feature", 236 | "properties": {}, 237 | "bbox": [1.0, 1.0, 3.0, 3.0] 238 | }] 239 | }) 240 | 241 | 242 | class ForeignKeyTest(TestCase): 243 | 244 | def setUp(self): 245 | self.route = Route.objects.create( 246 | name='green', geom="LINESTRING (0 0, 1 1)") 247 | Sign(label='A', route=self.route).save() 248 | 249 | def test_serialize_foreign(self): 250 | serializer = Serializer() 251 | features = json.loads(serializer.serialize(Sign.objects.all(), properties=['route'])) 252 | self.assertEqual( 253 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [0.5, 0.5]}, "type": "Feature", "properties": {"route": 1, "model": "djgeojson.sign"}, "id": self.route.pk}]}) 254 | 255 | def test_serialize_foreign_natural(self): 256 | serializer = Serializer() 257 | features = json.loads(serializer.serialize( 258 | Sign.objects.all(), use_natural_keys=True, properties=['route'])) 259 | self.assertEqual( 260 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "Point", "coordinates": [0.5, 0.5]}, "type": "Feature", "properties": {"route": "green", "model": "djgeojson.sign"}, "id": self.route.pk}]}) 261 | 262 | 263 | class ManyToManyTest(TestCase): 264 | 265 | def setUp(self): 266 | country1 = Country(label='C1', geom="POLYGON ((0 0,1 1,0 2,0 0))") 267 | country1.save() 268 | country2 = Country(label='C2', geom="POLYGON ((0 0,1 1,0 2,0 0))") 269 | country2.save() 270 | 271 | self.route1 = Route.objects.create( 272 | name='green', geom="LINESTRING (0 0, 1 1)") 273 | self.route2 = Route.objects.create( 274 | name='blue', geom="LINESTRING (0 0, 1 1)") 275 | self.route2.countries.add(country1) 276 | self.route3 = Route.objects.create( 277 | name='red', geom="LINESTRING (0 0, 1 1)") 278 | self.route3.countries.add(country1) 279 | self.route3.countries.add(country2) 280 | 281 | def test_serialize_manytomany(self): 282 | serializer = Serializer() 283 | features = json.loads(serializer.serialize( 284 | Route.objects.all(), properties=['countries'])) 285 | self.assertEqual( 286 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": []}, "id": self.route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": [1]}, "id": self.route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": [1, 2]}, "id": self.route3.pk}]}) 287 | 288 | def test_serialize_manytomany_natural(self): 289 | serializer = Serializer() 290 | features = json.loads(serializer.serialize( 291 | Route.objects.all(), use_natural_keys=True, properties=['countries'])) 292 | self.assertEqual( 293 | features, {"crs": {"type": "link", "properties": {"href": "http://spatialreference.org/ref/epsg/4326/", "type": "proj4"}}, "type": "FeatureCollection", "features": [{"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": []}, "id": self.route1.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": ["C1"]}, "id": self.route2.pk}, {"geometry": {"type": "LineString", "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, "type": "Feature", "properties": {"model": "djgeojson.route", "countries": ["C1", "C2"]}, "id": self.route3.pk}]}) 294 | 295 | 296 | class ReverseForeignkeyTest(TestCase): 297 | 298 | def setUp(self): 299 | self.route = Route(name='green', geom="LINESTRING (0 0, 1 1)") 300 | self.route.save() 301 | self.sign1 = Sign.objects.create(label='A', route=self.route) 302 | self.sign2 = Sign.objects.create(label='B', route=self.route) 303 | self.sign3 = Sign.objects.create(label='C', route=self.route) 304 | 305 | def test_relation_set(self): 306 | self.assertEqual(len(self.route.signs.all()), 3) 307 | 308 | def test_serialize_reverse(self): 309 | serializer = Serializer() 310 | features = json.loads(serializer.serialize( 311 | Route.objects.all(), properties=['signs'])) 312 | self.assertEqual( 313 | features, { 314 | "crs": { 315 | "type": "link", "properties": { 316 | "href": "http://spatialreference.org/ref/epsg/4326/", 317 | "type": "proj4" 318 | } 319 | }, 320 | "type": "FeatureCollection", 321 | "features": [{ 322 | "geometry": { 323 | "type": "LineString", 324 | "coordinates": [[0.0, 0.0], [1.0, 1.0]] 325 | }, 326 | "type": "Feature", 327 | "properties": { 328 | "model": "djgeojson.route", 329 | "signs": [ 330 | self.sign1.pk, 331 | self.sign2.pk, 332 | self.sign3.pk]}, 333 | "id": self.route.pk 334 | }] 335 | }) 336 | 337 | def test_serialize_reverse_natural(self): 338 | serializer = Serializer() 339 | features = json.loads(serializer.serialize( 340 | Route.objects.all(), use_natural_keys=True, properties=['signs'])) 341 | self.assertEqual( 342 | features, { 343 | "crs": { 344 | "type": "link", 345 | "properties": { 346 | "href": "http://spatialreference.org/ref/epsg/4326/", 347 | "type": "proj4" 348 | } 349 | }, 350 | "type": "FeatureCollection", 351 | "features": [{ 352 | "geometry": { 353 | "type": "LineString", 354 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, 355 | "type": "Feature", 356 | "properties": { 357 | "model": "djgeojson.route", 358 | "signs": ["A", "B", "C"]}, 359 | "id": self.route.pk 360 | }] 361 | }) 362 | 363 | 364 | class GeoJsonTemplateTagTest(TestCase): 365 | 366 | def setUp(self): 367 | self.route1 = Route.objects.create(name='green', 368 | geom="LINESTRING (0 0, 1 1)") 369 | self.route2 = Route.objects.create(name='blue', 370 | geom="LINESTRING (0 0, 1 1)") 371 | self.route3 = Route.objects.create(name='red', 372 | geom="LINESTRING (0 0, 1 1)") 373 | 374 | def test_templatetag_renders_single_object(self): 375 | feature = json.loads(geojsonfeature(self.route1)) 376 | self.assertEqual( 377 | feature, { 378 | "crs": { 379 | "type": "link", 380 | "properties": { 381 | "href": "http://spatialreference.org/ref/epsg/4326/", 382 | "type": "proj4" 383 | } 384 | }, 385 | "type": "FeatureCollection", 386 | "features": [{ 387 | "geometry": { 388 | "type": "LineString", 389 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, 390 | "type": "Feature", "properties": {}}] 391 | }) 392 | 393 | def test_templatetag_renders_queryset(self): 394 | feature = json.loads(geojsonfeature(Route.objects.all())) 395 | self.assertEqual( 396 | feature, { 397 | "crs": { 398 | "type": "link", "properties": { 399 | "href": "http://spatialreference.org/ref/epsg/4326/", 400 | "type": "proj4" 401 | } 402 | }, 403 | "type": "FeatureCollection", 404 | "features": [ 405 | { 406 | "geometry": { 407 | "type": "LineString", 408 | "coordinates": [[0.0, 0.0], [1.0, 1.0]] 409 | }, 410 | "type": "Feature", 411 | "properties": { 412 | "model": "djgeojson.route" 413 | }, 414 | "id": self.route1.pk 415 | }, 416 | { 417 | "geometry": { 418 | "type": "LineString", 419 | "coordinates": [[0.0, 0.0], [1.0, 1.0]] 420 | }, 421 | "type": "Feature", 422 | "properties": {"model": "djgeojson.route"}, 423 | "id": self.route2.pk 424 | }, 425 | { 426 | "geometry": {"type": "LineString", 427 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, 428 | "type": "Feature", 429 | "properties": {"model": "djgeojson.route"}, 430 | "id": self.route3.pk 431 | } 432 | ] 433 | }) 434 | 435 | def test_template_renders_geometry(self): 436 | feature = json.loads(geojsonfeature(self.route1.geom)) 437 | self.assertEqual( 438 | feature, { 439 | "geometry": {"type": "LineString", 440 | "coordinates": [[0.0, 0.0], [1.0, 1.0]]}, 441 | "type": "Feature", "properties": {} 442 | }) 443 | 444 | def test_property_can_be_specified(self): 445 | features = json.loads(geojsonfeature(self.route1, 446 | "name")) 447 | feature = features['features'][0] 448 | self.assertEqual(feature['properties']['name'], 449 | self.route1.name) 450 | 451 | def test_several_properties_can_be_specified(self): 452 | features = json.loads(geojsonfeature(self.route1, 453 | "name,id")) 454 | feature = features['features'][0] 455 | self.assertEqual(feature['properties'], 456 | {'name': self.route1.name, 457 | 'id': self.route1.id}) 458 | 459 | def test_srid_can_be_specified(self): 460 | feature = json.loads(geojsonfeature(self.route1.geom, "::2154")) 461 | self.assertAlmostEqual(feature['geometry']['coordinates'][0][0], 253531.1305237495) 462 | self.assertAlmostEqual(feature['geometry']['coordinates'][0][1], 909838.9305578759) 463 | self.assertAlmostEqual(feature['geometry']['coordinates'][1][0], 406035.7627716485) 464 | self.assertAlmostEqual(feature['geometry']['coordinates'][1][1], 1052023.2925472297) 465 | 466 | def test_geom_field_name_can_be_specified(self): 467 | features = json.loads(geojsonfeature(self.route1, ":geom")) 468 | feature = features['features'][0] 469 | self.assertEqual(feature['geometry']['coordinates'], 470 | [[0.0, 0.0], [1.0, 1.0]]) 471 | 472 | def test_geom_field_raises_attributeerror_if_unknown(self): 473 | self.assertRaises(AttributeError, geojsonfeature, self.route1, ":geo") 474 | 475 | 476 | class ViewsTest(TestCase): 477 | 478 | def setUp(self): 479 | self.route = Route.objects.create( 480 | name='green', geom="LINESTRING (0 0, 1 1)") 481 | Sign(label='A', route=self.route).save() 482 | 483 | def test_view_default_options(self): 484 | view = GeoJSONLayerView(model=Route) 485 | view.object_list = [] 486 | response = view.render_to_response(context={}) 487 | geojson = json.loads(smart_str(response.content)) 488 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'], 489 | [[0.0, 0.0], [1.0, 1.0]]) 490 | 491 | def test_view_can_control_properties(self): 492 | class FullGeoJSON(GeoJSONLayerView): 493 | properties = ['name'] 494 | view = FullGeoJSON(model=Route) 495 | view.object_list = [] 496 | response = view.render_to_response(context={}) 497 | geojson = json.loads(smart_str(response.content)) 498 | self.assertEqual(geojson['features'][0]['properties']['name'], 499 | 'green') 500 | 501 | def test_view_foreign(self): 502 | class FullGeoJSON(GeoJSONLayerView): 503 | properties = ['label', 'route'] 504 | view = FullGeoJSON(model=Sign) 505 | view.object_list = [] 506 | response = view.render_to_response(context={}) 507 | geojson = json.loads(smart_str(response.content)) 508 | self.assertEqual(geojson['features'][0]['properties']['route'], 509 | 1) 510 | 511 | def test_view_foreign_natural(self): 512 | class FullGeoJSON(GeoJSONLayerView): 513 | properties = ['label', 'route'] 514 | use_natural_keys = True 515 | view = FullGeoJSON(model=Sign) 516 | view.object_list = [] 517 | response = view.render_to_response(context={}) 518 | geojson = json.loads(smart_str(response.content)) 519 | self.assertEqual(geojson['features'][0]['properties']['route'], 520 | 'green') 521 | 522 | 523 | class TileEnvelopTest(TestCase): 524 | def setUp(self): 525 | self.view = TiledGeoJSONLayerView() 526 | 527 | def test_raises_error_if_not_spherical_mercator(self): 528 | self.view.tile_srid = 2154 529 | self.assertRaises(AssertionError, self.view.tile_coord, 0, 0, 0) 530 | 531 | def test_origin_is_north_west_for_tile_0(self): 532 | self.assertEqual((-180.0, 85.0511287798066), 533 | self.view.tile_coord(0, 0, 0)) 534 | 535 | def test_origin_is_center_for_middle_tile(self): 536 | self.assertEqual((0, 0), self.view.tile_coord(8, 8, 4)) 537 | 538 | 539 | class TiledGeoJSONViewTest(TestCase): 540 | def setUp(self): 541 | self.view = TiledGeoJSONLayerView(model=Route) 542 | self.view.args = [] 543 | self.r1 = Route.objects.create(geom=LineString((0, 1), (10, 1))) 544 | self.r2 = Route.objects.create(geom=LineString((0, -1), (-10, -1))) 545 | 546 | def test_view_with_kwargs(self): 547 | self.view.kwargs = {'z': 4, 548 | 'x': 8, 549 | 'y': 7} 550 | response = self.view.render_to_response(context={}) 551 | geojson = json.loads(smart_str(response.content)) 552 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'], [[0.0, 1.0], [10.0, 1.0]]) 553 | 554 | def test_view_with_kwargs_wrong_type_z(self): 555 | self.view.kwargs = {'z': 'a', 556 | 'x': 8, 557 | 'y': 7} 558 | self.assertRaises(SuspiciousOperation, 559 | self.view.render_to_response, 560 | context={}) 561 | 562 | def test_view_with_kwargs_wrong_type_x(self): 563 | self.view.kwargs = {'z': 1, 564 | 'x': 'a', 565 | 'y': 7} 566 | self.assertRaises(SuspiciousOperation, 567 | self.view.render_to_response, 568 | context={}) 569 | 570 | def test_view_with_kwargs_wrong_type_y(self): 571 | self.view.kwargs = {'z': 4, 572 | 'x': 8, 573 | 'y': 'a'} 574 | self.assertRaises(SuspiciousOperation, 575 | self.view.render_to_response, 576 | context={}) 577 | 578 | def test_view_with_kwargs_no_z(self): 579 | self.view.kwargs = {'x': 8, 580 | 'y': 7} 581 | self.assertRaises(SuspiciousOperation, 582 | self.view.render_to_response, 583 | context={}) 584 | 585 | def test_view_with_kwargs_no_x(self): 586 | self.view.kwargs = {'z': 8, 587 | 'y': 7} 588 | self.assertRaises(SuspiciousOperation, 589 | self.view.render_to_response, 590 | context={}) 591 | 592 | def test_view_with_kwargs_no_y(self): 593 | self.view.kwargs = {'x': 8, 594 | 'z': 7} 595 | self.assertRaises(SuspiciousOperation, 596 | self.view.render_to_response, 597 | context={}) 598 | 599 | def test_view_is_serialized_as_geojson(self): 600 | self.view.args = [4, 8, 7] 601 | response = self.view.render_to_response(context={}) 602 | geojson = json.loads(smart_str(response.content)) 603 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'], 604 | [[0.0, 1.0], [10.0, 1.0]]) 605 | 606 | def test_view_trims_to_geometries_boundaries(self): 607 | self.view.args = [8, 128, 127] 608 | response = self.view.render_to_response(context={}) 609 | geojson = json.loads(smart_str(response.content)) 610 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'], 611 | [[0.0, 1.0], [1.40625, 1.0]]) 612 | 613 | def test_geometries_trim_can_be_disabled(self): 614 | self.view.args = [8, 128, 127] 615 | self.view.trim_to_boundary = False 616 | response = self.view.render_to_response(context={}) 617 | geojson = json.loads(smart_str(response.content)) 618 | self.assertEqual(geojson['features'][0]['geometry']['coordinates'], 619 | [[0.0, 1.0], [10.0, 1.0]]) 620 | 621 | def test_tile_extent_is_provided_in_collection(self): 622 | self.view.args = [8, 128, 127] 623 | response = self.view.render_to_response(context={}) 624 | geojson = json.loads(smart_str(response.content)) 625 | self.assertEqual(geojson['bbox'], 626 | [0.0, 0.0, 1.40625, 1.4061088354351565]) 627 | 628 | def test_url_parameters_are_converted_to_int(self): 629 | self.view.args = ['0', '0', '0'] 630 | self.assertEqual(2, len(self.view.get_queryset())) 631 | 632 | def test_zoom_0_queryset_contains_all(self): 633 | self.view.args = [0, 0, 0] 634 | self.assertEqual(2, len(self.view.get_queryset())) 635 | 636 | def test_zoom_4_filters_by_tile_extent(self): 637 | self.view.args = [4, 8, 7] 638 | self.assertEqual([self.r1], list(self.view.get_queryset())) 639 | 640 | def test_some_tiles_have_empty_queryset(self): 641 | self.view.args = [4, 6, 8] 642 | self.assertEqual(0, len(self.view.get_queryset())) 643 | 644 | def test_simplification_depends_on_zoom_level(self): 645 | self.view.simplifications = {6: 100} 646 | self.view.args = [6, 8, 4] 647 | self.view.get_queryset() 648 | self.assertEqual(self.view.simplify, 100) 649 | 650 | def test_simplification_is_default_if_not_specified(self): 651 | self.view.simplifications = {} 652 | self.view.args = [0, 8, 4] 653 | self.view.get_queryset() 654 | self.assertEqual(self.view.simplify, None) 655 | 656 | def test_simplification_takes_the_closest_upper_level(self): 657 | self.view.simplifications = {3: 100, 6: 200} 658 | self.view.args = [4, 8, 4] 659 | self.view.get_queryset() 660 | self.assertEqual(self.view.simplify, 200) 661 | 662 | 663 | class FixedSridPoint(models.Model): 664 | 665 | geom = models.PointField(srid=28992) 666 | 667 | 668 | class TiledGeoJSONViewFixedSridTest(TestCase): 669 | def setUp(self): 670 | self.view = TiledGeoJSONLayerView(model=FixedSridPoint) 671 | self.view.args = [] 672 | self.p1 = FixedSridPoint.objects.create(geom=Point(253286, 531490)) 673 | self.p2 = FixedSridPoint.objects.create(geom=Point(253442, 532897)) 674 | 675 | def test_within_viewport(self): 676 | self.view.args = [12, 2125, 1338] 677 | response = self.view.render_to_response(context={}) 678 | geojson = json.loads(smart_str(response.content)) 679 | self.assertEqual(len(geojson['features']), 2) 680 | self.assertAlmostEqual(geojson['features'][0]['geometry']['coordinates'][0], 6.843321961076886) 681 | self.assertAlmostEqual(geojson['features'][0]['geometry']['coordinates'][1], 52.76181518632031) 682 | self.assertAlmostEqual(geojson['features'][1]['geometry']['coordinates'][0], 6.846053240233331) 683 | self.assertAlmostEqual(geojson['features'][1]['geometry']['coordinates'][1], 52.77442791046052) 684 | 685 | 686 | class Address(models.Model): 687 | geom = GeoJSONField() 688 | 689 | 690 | class ModelFieldTest(TestCase): 691 | def setUp(self): 692 | self.address = Address() 693 | self.address.geom = {'type': 'Point', 'coordinates': [0, 0]} 694 | self.address.save() 695 | 696 | def test_models_can_have_geojson_fields(self): 697 | saved = Address.objects.get(id=self.address.id) 698 | if isinstance(saved.geom, dict): 699 | self.assertDictEqual(saved.geom, self.address.geom) 700 | else: 701 | # Django 1.8 ! 702 | self.assertEqual(json.loads(saved.geom.geojson), self.address.geom) 703 | 704 | def test_default_form_field_is_geojsonfield(self): 705 | field = self.address._meta.get_field('geom').formfield() 706 | self.assertTrue(isinstance(field, GeoJSONFormField)) 707 | 708 | def test_default_form_field_widget_is_hidden(self): 709 | field = self.address._meta.get_field('geom').formfield() 710 | self.assertTrue(isinstance(field.widget, HiddenInput)) 711 | 712 | def test_default_form_field_has_geojson_validator(self): 713 | field = self.address._meta.get_field('geom').formfield() 714 | validator = field.validators[0] 715 | self.assertTrue(isinstance(validator, GeoJSONValidator)) 716 | 717 | def test_form_field_raises_if_invalid_type(self): 718 | field = self.address._meta.get_field('geom').formfield() 719 | self.assertRaises(ValidationError, field.clean, 720 | {'type': 'FeatureCollection', 'foo': 'bar'}) 721 | 722 | def test_form_field_raises_if_type_missing(self): 723 | field = self.address._meta.get_field('geom').formfield() 724 | self.assertRaises(ValidationError, field.clean, 725 | {'foo': 'bar'}) 726 | 727 | def test_field_can_be_serialized(self): 728 | serializer = Serializer() 729 | geojson = serializer.serialize(Address.objects.all(), crs=False) 730 | features = json.loads(geojson) 731 | self.assertEqual( 732 | features, { 733 | 'type': u'FeatureCollection', 734 | 'features': [{ 735 | 'id': self.address.id, 736 | 'type': 'Feature', 737 | 'geometry': {'type': 'Point', 'coordinates': [0, 0]}, 738 | 'properties': { 739 | 'model': 'djgeojson.address' 740 | } 741 | }] 742 | }) 743 | 744 | def test_field_can_be_deserialized(self): 745 | input_geojson = """ 746 | {"type": "FeatureCollection", 747 | "features": [ 748 | { "type": "Feature", 749 | "properties": {"model": "djgeojson.address"}, 750 | "id": 1, 751 | "geometry": { 752 | "type": "Point", 753 | "coordinates": [0.0, 0.0] 754 | } 755 | } 756 | ]}""" 757 | objects = list(serializers.deserialize('geojson', input_geojson)) 758 | self.assertEqual(objects[0].object.geom, 759 | {'type': 'Point', 'coordinates': [0, 0]}) 760 | 761 | def test_model_can_be_omitted(self): 762 | serializer = Serializer() 763 | geojson = serializer.serialize(Address.objects.all(), 764 | with_modelname=False) 765 | features = json.loads(geojson) 766 | self.assertEqual( 767 | features, { 768 | "crs": { 769 | "type": "link", 770 | "properties": { 771 | "href": "http://spatialreference.org/ref/epsg/4326/", 772 | "type": "proj4" 773 | } 774 | }, 775 | 'type': 'FeatureCollection', 776 | 'features': [{ 777 | 'id': self.address.id, 778 | 'type': 'Feature', 779 | 'geometry': {'type': 'Point', 'coordinates': [0, 0]}, 780 | 'properties': {} 781 | }] 782 | }) 783 | 784 | 785 | class GeoJSONValidatorTest(TestCase): 786 | def test_validator_raises_if_missing_type(self): 787 | validator = GeoJSONValidator('GEOMETRY') 788 | self.assertRaises(ValidationError, validator, {'foo': 'bar'}) 789 | 790 | def test_validator_raises_if_type_is_wrong(self): 791 | validator = GeoJSONValidator('GEOMETRY') 792 | self.assertRaises(ValidationError, validator, 793 | {'type': 'FeatureCollection', 794 | 'features': []}) 795 | 796 | def test_validator_succeeds_if_type_matches(self): 797 | validator = GeoJSONValidator('POINT') 798 | self.assertIsNone(validator({'type': 'Point', 'coords': [0, 0]})) 799 | 800 | def test_validator_succeeds_if_type_is_generic(self): 801 | validator = GeoJSONValidator('GEOMETRY') 802 | self.assertIsNone(validator({'type': 'Point', 'coords': [0, 0]})) 803 | self.assertIsNone(validator({'type': 'LineString', 'coords': [0, 0]})) 804 | self.assertIsNone(validator({'type': 'Polygon', 'coords': [0, 0]})) 805 | 806 | def test_validator_fails_if_type_does_not_match(self): 807 | validator = GeoJSONValidator('POINT') 808 | self.assertRaises(ValidationError, validator, 809 | {'type': 'LineString', 'coords': [0, 0]}) 810 | -------------------------------------------------------------------------------- /djgeojson/views.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import django 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | try: 7 | from django.contrib.gis.db.models.functions import Intersection 8 | except (ImportError, ImproperlyConfigured): 9 | Intersection = None 10 | from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation 11 | from django.utils.decorators import method_decorator 12 | from django.views.decorators.gzip import gzip_page 13 | from django.views.generic import ListView 14 | 15 | try: 16 | from django.contrib.gis.geos import Polygon 17 | except (ImportError, ImproperlyConfigured): 18 | try: 19 | from django.contrib.gis.geos.geometry import Polygon 20 | except (ImportError, ImproperlyConfigured): 21 | from .nogeos import Polygon 22 | 23 | try: 24 | from django.contrib.gis.db.models import PointField 25 | except (ImportError, ImproperlyConfigured): 26 | from .fields import PointField 27 | 28 | from . import GEOJSON_DEFAULT_SRID 29 | from .http import HttpGeoJSONResponse 30 | from .serializers import Serializer as GeoJSONSerializer 31 | 32 | 33 | class GeoJSONResponseMixin(object): 34 | """ 35 | A mixin that can be used to render a GeoJSON response. 36 | """ 37 | response_class = HttpGeoJSONResponse 38 | """ Select fields for properties """ 39 | properties = [] 40 | """ Limit float precision """ 41 | precision = None 42 | """ Simplify geometries """ 43 | simplify = None 44 | """ Change projection of geometries """ 45 | srid = GEOJSON_DEFAULT_SRID 46 | """ Geometry field to serialize """ 47 | geometry_field = 'geom' 48 | """ Force 2D """ 49 | force2d = False 50 | """ bbox """ 51 | bbox = None 52 | """ bbox auto """ 53 | bbox_auto = False 54 | 55 | use_natural_keys = False 56 | 57 | with_modelname = True 58 | 59 | crs_type = 'name' 60 | 61 | def render_to_response(self, context, **response_kwargs): 62 | """ 63 | Returns a JSON response, transforming 'context' to make the payload. 64 | """ 65 | serializer = GeoJSONSerializer() 66 | response = self.response_class(**response_kwargs) 67 | queryset = self.get_queryset() 68 | 69 | options = dict(properties=self.properties, 70 | precision=self.precision, 71 | simplify=self.simplify, 72 | srid=self.srid, 73 | geometry_field=self.geometry_field, 74 | force2d=self.force2d, 75 | bbox=self.bbox, 76 | bbox_auto=self.bbox_auto, 77 | use_natural_keys=self.use_natural_keys, 78 | with_modelname=self.with_modelname, 79 | crs_type=self.crs_type) 80 | serializer.serialize(queryset, stream=response, ensure_ascii=False, 81 | **options) 82 | return response 83 | 84 | 85 | class GeoJSONLayerView(GeoJSONResponseMixin, ListView): 86 | """ 87 | A generic view to serve a model as a layer. 88 | """ 89 | @method_decorator(gzip_page) 90 | def dispatch(self, *args, **kwargs): 91 | return super(GeoJSONLayerView, self).dispatch(*args, **kwargs) 92 | 93 | 94 | class TiledGeoJSONLayerView(GeoJSONLayerView): 95 | width = 256 96 | height = 256 97 | tile_srid = 3857 98 | trim_to_boundary = True 99 | """Simplify geometries by zoom level (dict )""" 100 | simplifications = None 101 | 102 | def tile_coord(self, xtile, ytile, zoom): 103 | """ 104 | This returns the NW-corner of the square. Use the function 105 | with xtile+1 and/or ytile+1 to get the other corners. 106 | With xtile+0.5 & ytile+0.5 it will return the center of the tile. 107 | http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_numbers_to_lon..2Flat._2 108 | """ 109 | assert self.tile_srid == 3857, 'Custom tile projection not supported yet' 110 | n = 2.0 ** zoom 111 | lon_deg = xtile / n * 360.0 - 180.0 112 | lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) 113 | lat_deg = math.degrees(lat_rad) 114 | return (lon_deg, lat_deg) 115 | 116 | def _parse_args(self): 117 | try: 118 | return [int(v) for v in (self.args[0], self.args[1], self.args[2])] 119 | except (ValueError, IndexError): 120 | try: 121 | return [int(v) for v in (self.kwargs['z'], 122 | self.kwargs['x'], 123 | self.kwargs['y'])] 124 | except (ValueError, TypeError, KeyError): 125 | # Raise suspicious, Django will return ``400 Bad Request``. 126 | error_msg = "Tile (z, x, y) parameters could not be processed." 127 | raise SuspiciousOperation(error_msg) 128 | 129 | def get_queryset(self): 130 | """ 131 | Inspired by Glen Roberton's django-geojson-tiles view 132 | """ 133 | self.z, self.x, self.y = self._parse_args() 134 | nw = self.tile_coord(self.x, self.y, self.z) 135 | se = self.tile_coord(self.x + 1, self.y + 1, self.z) 136 | bbox = Polygon((nw, (se[0], nw[1]), 137 | se, (nw[0], se[1]), nw)) 138 | bbox.srid = self.srid 139 | qs = super(TiledGeoJSONLayerView, self).get_queryset() 140 | qs = qs.filter(**{ 141 | '%s__intersects' % self.geometry_field: bbox 142 | }) 143 | self.bbox = bbox.extent 144 | 145 | # Simplification dict by zoom level 146 | simplifications = self.simplifications or {} 147 | z = self.z 148 | self.simplify = simplifications.get(z) 149 | while self.simplify is None and z < 32: 150 | z += 1 151 | self.simplify = simplifications.get(z) 152 | 153 | # Won't trim point geometries to a boundary 154 | model_field = qs.model._meta.get_field(self.geometry_field) 155 | self.trim_to_boundary = (self.trim_to_boundary and 156 | not isinstance(model_field, PointField) and 157 | Intersection is not None) 158 | if self.trim_to_boundary: 159 | if django.VERSION < (1, 9): 160 | qs = qs.intersection(bbox) 161 | else: 162 | qs = qs.annotate(intersection=Intersection(self.geometry_field, bbox)) 163 | self.geometry_field = 'intersection' 164 | 165 | return qs 166 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-geojson.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-geojson.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-geojson" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-geojson" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced usage 2 | ============== 3 | 4 | Low-level serializer 5 | -------------------- 6 | 7 | :: 8 | 9 | from djgeojson.serializers import Serializer as GeoJSONSerializer 10 | 11 | GeoJSONSerializer().serialize(Restaurants.objects.all(), use_natural_keys=True, with_modelname=False) 12 | 13 | 14 | 15 | Low-level deserializer 16 | ---------------------- 17 | 18 | :: 19 | 20 | from djgeojson.serializers import Serializer as GeoJSONSerializer 21 | 22 | GeoJSONSerializer().deserialize('geojson', my_geojson) 23 | 24 | You can optionally specify the model name directly in the parameters: 25 | 26 | :: 27 | 28 | GeoJSONSerializer().deserialize('geojson', my_geojson, model_name=my_model_name) 29 | 30 | 31 | 32 | 33 | Dump GIS models, or fixtures 34 | ---------------------------- 35 | 36 | Register the serializer in your project : 37 | 38 | :: 39 | 40 | # settings.py 41 | 42 | SERIALIZATION_MODULES = { 43 | 'geojson' : 'djgeojson.serializers' 44 | } 45 | 46 | Command-line ``dumpdata`` can export files, viewable in GIS software like QGis : 47 | 48 | :: 49 | 50 | python manage.py dumpdata --format=geojson yourapp.Model > export.geojson 51 | 52 | Works with ``loaddata`` as well, which can now import GeoJSON files. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # django-geojson documentation build configuration file, created by 3 | # sphinx-quickstart on Tue Feb 28 13:44:49 2017. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ------------------------------------------------ 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'django-geojson' 46 | copyright = u'2017, Makina Corpus' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = '2.10' 54 | # The full version, including alpha/beta/rc tags. 55 | release = '2.10' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all 72 | # documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | # If true, keep warnings as "system message" paragraphs in the built documents. 93 | #keep_warnings = False 94 | 95 | 96 | # -- Options for HTML output ---------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'default' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | #html_theme_path = [] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # Add any extra paths that contain custom files (such as robots.txt or 132 | # .htaccess) here, relative to this directory. These files are copied 133 | # directly to the root of the documentation. 134 | #html_extra_path = [] 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | #html_last_updated_fmt = '%b %d, %Y' 139 | 140 | # If true, SmartyPants will be used to convert quotes and dashes to 141 | # typographically correct entities. 142 | #html_use_smartypants = True 143 | 144 | # Custom sidebar templates, maps document names to template names. 145 | #html_sidebars = {} 146 | 147 | # Additional templates that should be rendered to pages, maps page names to 148 | # template names. 149 | #html_additional_pages = {} 150 | 151 | # If false, no module index is generated. 152 | #html_domain_indices = True 153 | 154 | # If false, no index is generated. 155 | #html_use_index = True 156 | 157 | # If true, the index is split into individual pages for each letter. 158 | #html_split_index = False 159 | 160 | # If true, links to the reST sources are added to the pages. 161 | #html_show_sourcelink = True 162 | 163 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 164 | #html_show_sphinx = True 165 | 166 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 167 | #html_show_copyright = True 168 | 169 | # If true, an OpenSearch description file will be output, and all pages will 170 | # contain a tag referring to it. The value of this option must be the 171 | # base URL from which the finished HTML is served. 172 | #html_use_opensearch = '' 173 | 174 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 175 | #html_file_suffix = None 176 | 177 | # Output file base name for HTML help builder. 178 | htmlhelp_basename = 'django-geojsondoc' 179 | 180 | 181 | # -- Options for LaTeX output --------------------------------------------- 182 | 183 | latex_elements = { 184 | # The paper size ('letterpaper' or 'a4paper'). 185 | #'papersize': 'letterpaper', 186 | 187 | # The font size ('10pt', '11pt' or '12pt'). 188 | #'pointsize': '10pt', 189 | 190 | # Additional stuff for the LaTeX preamble. 191 | #'preamble': '', 192 | } 193 | 194 | # Grouping the document tree into LaTeX files. List of tuples 195 | # (source start file, target name, title, 196 | # author, documentclass [howto, manual, or own class]). 197 | latex_documents = [ 198 | ('index', 'django-geojson.tex', u'django-geojson Documentation', 199 | u'Makina Corpus', 'manual'), 200 | ] 201 | 202 | # The name of an image file (relative to this directory) to place at the top of 203 | # the title page. 204 | #latex_logo = None 205 | 206 | # For "manual" documents, if this is true, then toplevel headings are parts, 207 | # not chapters. 208 | #latex_use_parts = False 209 | 210 | # If true, show page references after internal links. 211 | #latex_show_pagerefs = False 212 | 213 | # If true, show URL addresses after external links. 214 | #latex_show_urls = False 215 | 216 | # Documents to append as an appendix to all manuals. 217 | #latex_appendices = [] 218 | 219 | # If false, no module index is generated. 220 | #latex_domain_indices = True 221 | 222 | 223 | # -- Options for manual page output --------------------------------------- 224 | 225 | # One entry per manual page. List of tuples 226 | # (source start file, name, description, authors, manual section). 227 | man_pages = [ 228 | ('index', 'django-geojson', u'django-geojson Documentation', 229 | [u'Makina Corpus'], 1) 230 | ] 231 | 232 | # If true, show URL addresses after external links. 233 | #man_show_urls = False 234 | 235 | 236 | # -- Options for Texinfo output ------------------------------------------- 237 | 238 | # Grouping the document tree into Texinfo files. List of tuples 239 | # (source start file, target name, title, author, 240 | # dir menu entry, description, category) 241 | texinfo_documents = [ 242 | ('index', 'django-geojson', u'django-geojson Documentation', 243 | u'Makina Corpus', 'django-geojson', 'One line description of project.', 244 | 'Miscellaneous'), 245 | ] 246 | 247 | # Documents to append as an appendix to all manuals. 248 | #texinfo_appendices = [] 249 | 250 | # If false, no module index is generated. 251 | #texinfo_domain_indices = True 252 | 253 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 254 | #texinfo_show_urls = 'footnote' 255 | 256 | # If true, do not generate a @detailmenu in the "Top" node's menu. 257 | #texinfo_no_detailmenu = False 258 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-geojson documentation master file, created by 2 | sphinx-quickstart on Tue Feb 28 13:44:49 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-geojson's documentation! 7 | ========================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | installation 15 | views 16 | models 17 | advanced 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | :: 5 | 6 | pip install django-geojson 7 | 8 | 9 | This package has **optional** `extra dependencies `_. 10 | 11 | 12 | If you need GeoJSON fields with map widgets : 13 | 14 | :: 15 | 16 | pip install "django-geojson [field]" 17 | 18 | Configuration 19 | ------------- 20 | 21 | Add ``djgeojson`` to your applications : 22 | 23 | :: 24 | 25 | # settings.py 26 | 27 | INSTALLED_APPS += ( 28 | 'djgeojson', 29 | ) 30 | 31 | *(not required for views)* -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-geojson.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-geojson.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/models.rst: -------------------------------------------------------------------------------- 1 | Model and forms fields 2 | ====================== 3 | 4 | GeoJSON fields are based on Django JSONField. 5 | See :doc:`installation` to install extra dependencies. 6 | 7 | They are useful to avoid usual GIS stacks (GEOS, GDAL, PostGIS...) 8 | for very simple use-cases (no spatial operation yet). 9 | 10 | :: 11 | 12 | from djgeojson.fields import PointField 13 | 14 | class Address(models.Model): 15 | geom = PointField() 16 | 17 | address = Address() 18 | address.geom = {'type': 'Point', 'coordinates': [0, 0]} 19 | address.save() 20 | 21 | 22 | Form widgets are rendered with Leaflet maps automatically if 23 | `django-leaflet `_ 24 | is available. 25 | 26 | All geometry types are supported and respectively validated : 27 | `GeometryField`, `PointField`, `MultiPointField`, `LineStringField`, 28 | `MultiLineStringField`, `PolygonField`, `MultiPolygonField`, 29 | `GeometryCollectionField`. 30 | -------------------------------------------------------------------------------- /docs/views.rst: -------------------------------------------------------------------------------- 1 | Views 2 | ===== 3 | 4 | GeoJSON layer view 5 | ------------------ 6 | 7 | Very useful for web mapping : 8 | 9 | :: 10 | 11 | # urls.py 12 | from djgeojson.views import GeoJSONLayerView 13 | ... 14 | url(r'^data.geojson$', GeoJSONLayerView.as_view(model=MushroomSpot), name='data'), 15 | 16 | 17 | Consume the vector layer as usual, for example, with Leaflet loaded with jQuery Ajax: 18 | 19 | :: 20 | 21 | # Leaflet JS 22 | var layer = L.geoJson(); 23 | map.addLayer(layer); 24 | $.getJSON("{% url 'data' %}", function (data) { 25 | layer.addData(data); 26 | }); 27 | 28 | 29 | Inherit generic views **only** if you need a reusable set of options : 30 | 31 | :: 32 | 33 | # views.py 34 | from djgeojson.views import GeoJSONLayerView 35 | 36 | class MapLayer(GeoJSONLayerView): 37 | # Options 38 | precision = 4 # float 39 | simplify = 0.5 # generalization 40 | 41 | 42 | # urls.py 43 | from .views import MapLayer, MeetingLayer 44 | ... 45 | url(r'^mushrooms.geojson$', MapLayer.as_view(model=MushroomSpot, properties=('name',)), name='mushrooms') 46 | 47 | Most common use-cases of reusable options are: low-fi precision, common list of fields between several views, etc. 48 | 49 | Options are : 50 | 51 | * **properties** : ``list`` of properties names, or ``dict`` for mapping field names and properties 52 | * **simplify** : generalization of geometries (See ``simplify()``) 53 | * **precision** : number of digit after comma 54 | * **geometry_field** : name of geometry field (*default*: ``geom``) 55 | * **srid** : projection (*default*: 4326, for WGS84) 56 | * **bbox** : Allows you to set your own bounding box on feature collection level 57 | * **bbox_auto** : True/False (default false). Will automatically generate a bounding box on a per feature level. 58 | * **use_natural_keys** : serialize natural keys instead of primary keys (*default*: ``False``) 59 | * **with_modelname** : add the app and model name to the properties. (*default*: ``True``) 60 | * **crs_type** : add the type of crs generated, options: ``name`` and ``link`` (*default*: ``name``) 61 | 62 | Tiled GeoJSON layer view 63 | ------------------------ 64 | 65 | Vectorial tiles can help display a great number of objects on the map, 66 | with `reasonnable performance `_. 67 | 68 | :: 69 | 70 | # urls.py 71 | from djgeojson.views import TiledGeoJSONLayerView 72 | ... 73 | 74 | url(r'^data/(?P\d+)/(?P\d+)/(?P\d+).geojson$', 75 | TiledGeoJSONLayerView.as_view(model=MushroomSpot), name='data'), 76 | 77 | 78 | Consume the vector tiles using `Leaflet TileLayer GeoJSON `_, `Polymaps `_, `OpenLayers 3 `_ or `d3.js `_ for example. 79 | 80 | Options are : 81 | 82 | * **trim_to_boundary** : if ``True`` geometries are trimmed to the tile boundary 83 | * **simplifications** : a dict of simplification values by zoom level 84 | 85 | 86 | 87 | GeoJSON template filter 88 | ----------------------- 89 | 90 | Mainly useful to dump features in HTML output and bypass AJAX call : 91 | 92 | :: 93 | 94 | // Leaflet JS 95 | var raw_data = '{{ object_list|geojsonfeature|safe|escapejs }}'; 96 | var data = JSON.parse(raw_data); 97 | L.geoJSON(data).addTo(map); 98 | 99 | Will work either for a model, a geometry field or a queryset. 100 | 101 | :: 102 | 103 | {% load geojson_tags %} 104 | 105 | var feature = {{ object|geojsonfeature|safe }}; 106 | 107 | var geom = {{ object.geom|geojsonfeature|safe }}; 108 | 109 | var collection = {{ object_list|geojsonfeature|safe }}; 110 | 111 | 112 | Properties and custom geometry field name can be provided. 113 | 114 | :: 115 | 116 | {{ object|geojsonfeature:"name,age" }} 117 | {{ object|geojsonfeature:"name,age:the_geom" }} 118 | {{ object|geojsonfeature:":geofield" }} 119 | -------------------------------------------------------------------------------- /quicktest.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | import django 6 | from django.conf import settings 7 | from django.test.runner import DiscoverRunner 8 | 9 | 10 | class QuickDjangoTest: 11 | """ 12 | A quick way to run the Django test suite without a fully-configured project. 13 | 14 | Example usage: 15 | 16 | >>> QuickDjangoTest('app1', 'app2') 17 | 18 | Based on a script published by Lukasz Dziedzia at: 19 | http://stackoverflow.com/questions/3841725/how-to-launch-tests-for-django-reusable-app 20 | """ 21 | DIRNAME = os.path.dirname(__file__) 22 | INSTALLED_APPS = ( 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.admin', 27 | 'django.contrib.messages', 28 | ) 29 | 30 | def __init__(self, *args, **kwargs): 31 | self.apps = args 32 | self.run_tests() 33 | 34 | def run_tests(self): 35 | """ 36 | Fire up the Django test suite developed for version 1.2 37 | """ 38 | settings.configure( 39 | DATABASES={ 40 | 'default': { 41 | 'ENGINE': 'django.contrib.gis.db.backends.spatialite', 42 | 'NAME': os.path.join(self.DIRNAME, 'database.db'), 43 | } 44 | }, 45 | INSTALLED_APPS=self.INSTALLED_APPS + self.apps, 46 | SPATIALITE_LIBRARY_PATH=os.getenv('SPATIALITE_LIBRARY_PATH', 'mod_spatialite'), 47 | TEMPLATES=[ 48 | { 49 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 50 | 'DIRS': [], 51 | 'APP_DIRS': True, 52 | 'OPTIONS': { 53 | "context_processors": [ 54 | "django.contrib.auth.context_processors.auth", 55 | "django.contrib.messages.context_processors.messages", 56 | "django.template.context_processors.request", 57 | ] 58 | }, 59 | }, 60 | ], 61 | MIDDLEWARE=[ 62 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 63 | 'django.contrib.messages.middleware.MessageMiddleware', 64 | 'django.contrib.sessions.middleware.SessionMiddleware', 65 | ], 66 | SECRET_KEY="not-secret", 67 | # django.VERSION >= (3, 2) 68 | DEFAULT_AUTO_FIELD="django.db.models.BigAutoField", 69 | ) 70 | 71 | django.setup() 72 | 73 | # Workaround for incompatibility between SQLite 3.36+ and SpatiaLite 5, 74 | # as used on GitHub Actions. This change monkey patches 75 | # prepare_database() to avoid a call to InitSpatialMetaDataFull(). See: 76 | # https://code.djangoproject.com/ticket/32935 77 | # https://groups.google.com/g/spatialite-users/c/SnNZt4AGm_o 78 | from django.contrib.gis.db.backends.spatialite.base import ( 79 | DatabaseWrapper, 80 | ) 81 | 82 | def prepare_database(self): 83 | super(DatabaseWrapper, self).prepare_database() 84 | with self.cursor() as cursor: 85 | cursor.execute("PRAGMA table_info(geometry_columns);") 86 | if cursor.fetchall() == []: 87 | cursor.execute("SELECT InitSpatialMetaData(1)") 88 | 89 | DatabaseWrapper.prepare_database = prepare_database 90 | 91 | failures = DiscoverRunner().run_tests(self.apps, verbosity=1) 92 | if failures: # pragma: no cover 93 | sys.exit(failures) 94 | 95 | if __name__ == '__main__': 96 | """ 97 | What do when the user hits this file from the shell. 98 | 99 | Example usage: 100 | 101 | $ python quicktest.py app1 app2 102 | 103 | """ 104 | parser = argparse.ArgumentParser( 105 | usage="[args]", 106 | description="Run Django tests on the provided applications." 107 | ) 108 | parser.add_argument('apps', nargs='+', type=str) 109 | args = parser.parse_args() 110 | QuickDjangoTest(*args.apps) 111 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import open 3 | 4 | from setuptools import find_packages, setup 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | setup( 9 | name='django-geojson', 10 | version='4.2.1dev', 11 | author='Mathieu Leplatre', 12 | author_email='mathieu.leplatre@makina-corpus.com', 13 | url='https://github.com/makinacorpus/django-geojson', 14 | download_url="http://pypi.python.org/pypi/django-geojson/", 15 | description="Serve vectorial map layers with Django", 16 | long_description=open(os.path.join(here, 'README.rst')).read() + '\n\n' + 17 | open(os.path.join(here, 'CHANGES'), encoding='utf-8').read(), 18 | license='LPGL, see LICENSE file.', 19 | install_requires=[ 20 | 'Django>=4.2', 21 | ], 22 | extras_require={ 23 | 'field': ['django-leaflet>=0.12'], 24 | 'docs': ['sphinx', 'sphinx-autobuild'], 25 | }, 26 | packages=find_packages(), 27 | python_requires=">=3.8", 28 | include_package_data=True, 29 | zip_safe=False, 30 | classifiers=['Topic :: Utilities', 31 | 'Natural Language :: English', 32 | 'Operating System :: OS Independent', 33 | 'Intended Audience :: Developers', 34 | 'Environment :: Web Environment', 35 | 'Framework :: Django', 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Programming Language :: Python :: 3.9', 38 | 'Programming Language :: Python :: 3.10', 39 | 'Programming Language :: Python :: 3.11', 40 | 'Programming Language :: Python :: 3.12', 41 | 'Programming Language :: Python :: 3.13', 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | envlist = 4 | flake8 5 | isort 6 | py39-django{40,41,42} 7 | py310-django{40,41,42,50,51,52} 8 | py311-django{41,42,50,51,52} 9 | py312-django{42,50,51,52} 10 | py313-django{51,52} 11 | 12 | [testenv] 13 | setenv = 14 | PYTHONPATH = {toxinidir}:{toxinidir} 15 | deps = 16 | coverage 17 | django40: Django>=4.0,<4.1 18 | django41: Django>=4.1,<4.2 19 | django42: Django>=4.2,<5.0 20 | django50: Django>=5.0,<5.1 21 | django51: Django>=5.1,<5.2 22 | django52: Django>=5.2a1,<5.3 23 | commands = 24 | python \ 25 | -W error::DeprecationWarning \ 26 | -W error::PendingDeprecationWarning \ 27 | -W ignore:::site \ 28 | -W ignore:::distutils \ 29 | -m coverage run \ 30 | {toxinidir}/quicktest.py djgeojson 31 | 32 | [testenv:docs] 33 | basepython = python 34 | changedir = docs 35 | deps = 36 | sphinx 37 | sphinx_rtd_theme 38 | Django>=5.1,<5.2 39 | commands = 40 | sphinx-build -W -b html -d build/doctrees . build/html 41 | 42 | [testenv:flake8] 43 | usedevelop = false 44 | basepython = python3.13 45 | deps = flake8 46 | commands = flake8 --ignore=E501,W504 djgeojson 47 | 48 | [testenv:isort] 49 | usedevelop = false 50 | basepython = python3.13 51 | deps = isort 52 | commands = isort . 53 | 54 | [isort] 55 | skip_gitignore = true 56 | include_trailing_comma = true 57 | multi_line_output = 3 58 | line_length = 80 59 | --------------------------------------------------------------------------------