Initial
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
|
||||
dist-env/
|
||||
dist/
|
||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright 2024 relikd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
recursive-include map_location/static *
|
||||
recursive-include map_location/templates *
|
||||
27
Makefile
Normal file
27
Makefile
Normal file
@@ -0,0 +1,27 @@
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo 'commands:'
|
||||
@echo ' install, dist'
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
[ -z "$${VIRTUAL_ENV}" ] \
|
||||
&& python3 -m pip install -e . --user \
|
||||
|| python3 -m pip install -e .
|
||||
|
||||
dist-env:
|
||||
@echo Creating virtual environment...
|
||||
@python3 -m venv 'dist-env'
|
||||
@source dist-env/bin/activate && pip install twine
|
||||
|
||||
.PHONY: dist
|
||||
dist: dist-env
|
||||
[ -z "$${VIRTUAL_ENV}" ] # you can not do this inside a virtual environment.
|
||||
rm -rf dist
|
||||
@echo Building...
|
||||
python3 setup.py sdist bdist_wheel
|
||||
@echo
|
||||
rm -rf ./*.egg-info/ ./build/
|
||||
@echo Publishing...
|
||||
@echo "\033[0;31mEnter your PyPI token:\033[0m"
|
||||
@source dist-env/bin/activate && export TWINE_USERNAME='__token__' && twine upload dist/*
|
||||
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Django: Map-Location
|
||||
|
||||
Adds a fully-static location field (Leaflet) to manage location-based data without the need for a full-fledged GeoDjango installation. ... when all you need is a visual position chooser.
|
||||
|
||||

|
||||
|
||||
|
||||
## Install
|
||||
|
||||
1. Add to your INSTALLED_APPS
|
||||
|
||||
```py
|
||||
INSTALLED_APPS = [
|
||||
...,
|
||||
'map_location',
|
||||
]
|
||||
```
|
||||
|
||||
2. Create Map-Location field
|
||||
|
||||
```py
|
||||
from map_location.fields import LocationField
|
||||
|
||||
class Place(models.Model):
|
||||
location = LocationField('Pos', blank=True, null=True, options={
|
||||
'map': {
|
||||
'center': [52.52, 13.40],
|
||||
'zoom': 12,
|
||||
},
|
||||
# 'markerZoom': 18
|
||||
# 'tileLayer': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
# 'tileOptions': {
|
||||
# attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
# },
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Options Paramter
|
||||
|
||||
| Key | Info
|
||||
|-------------|------------------
|
||||
| map | [Map Options](https://leafletjs.com/reference.html#map-option) (default: `{center: [20, -25], zoom: 2}`)
|
||||
| markerZoom | Initial zoom scale (on load) – if a marker is set. (default: `18`)
|
||||
| tileLayer | [TileLayer](https://leafletjs.com/reference.html#tilelayer) urlTemplate (default: `"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"`)
|
||||
| tileOptions | [TileLayer Options](https://leafletjs.com/reference.html#tilelayer-option) (default: `{}`)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
You can access the location by its parts (`place.location.lat` & `place.location.long`) or by its string value (`str(place.location)` or just `place.location`) which will return a comma-separated string (`lat,long`). This string format is also used for database storage (with a precision of 6 digits, or up to 11 cm).
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
If you export your location as json, you can use a fully static map:
|
||||
|
||||
```py
|
||||
_JSON_ = {'loc': [place.location.lat, place.location.long]}
|
||||
```
|
||||
|
||||
```html
|
||||
<script src="/static/leaflet/leaflet.js"></script>
|
||||
<script src="/static/leaflet/locate/L.Control.Locate.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/leaflet/leaflet.css" />
|
||||
<link rel="stylesheet" href="/static/leaflet/locate/L.Control.Locate.css" />
|
||||
...
|
||||
<div id="map-id"></div>
|
||||
...
|
||||
<script>
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
|
||||
const map = L.map('map-id', {
|
||||
layers: [osm],
|
||||
center: [52.52, 13.40],
|
||||
zoom: 14,
|
||||
});
|
||||
L.control.locate({
|
||||
returnToPrevBounds: true,
|
||||
showPopup: false,
|
||||
}).addTo(map);
|
||||
L.marker(L.latLng(_JSON_.loc)).addTo(map);
|
||||
...
|
||||
</script>
|
||||
```
|
||||
|
||||
See [Leaflet](https://leafletjs.com/) docs for configuration options.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under MIT and includes:
|
||||
|
||||
- v1.9.4 [Leaflet](https://github.com/Leaflet/Leaflet) (BSD 2-Clause License)
|
||||
- v0.81.1 [Leaflet.Locate](https://github.com/domoritz/leaflet-locatecontrol) (MIT)
|
||||
0
map_location/__init__.py
Normal file
0
map_location/__init__.py
Normal file
7
map_location/apps.py
Normal file
7
map_location/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MapLocationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'map_location'
|
||||
verbose_name = 'Map Location'
|
||||
78
map_location/fields.py
Normal file
78
map_location/fields.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from typing import NamedTuple
|
||||
from django.db import models
|
||||
from django.forms import Widget
|
||||
|
||||
|
||||
class Position(NamedTuple):
|
||||
lat: float
|
||||
long: float
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.lat:.6f},{self.long:.6f}'
|
||||
|
||||
|
||||
class MapLocationWidget(Widget):
|
||||
template_name = 'forms/map-location.html'
|
||||
|
||||
class Media:
|
||||
# @see https://github.com/Leaflet/Leaflet
|
||||
# @see https://github.com/domoritz/leaflet-locatecontrol
|
||||
css = {'all': [
|
||||
'leaflet/leaflet.css',
|
||||
'leaflet/locate/L.Control.Locate.css']}
|
||||
js = ['leaflet/leaflet.js',
|
||||
'leaflet/locate/L.Control.Locate.min.js',
|
||||
'map-location.js']
|
||||
|
||||
# def get_context(self, name, value, attrs):
|
||||
# context = super().get_context(name, value, attrs)
|
||||
# context['id'] = attrs.get('id')
|
||||
# return context
|
||||
|
||||
|
||||
class LocationField(models.Field):
|
||||
description = 'Choose location on map'
|
||||
|
||||
def __init__(self, *args, options={}, **kwargs):
|
||||
self.options = options
|
||||
kwargs['max_length'] = 22 # e.g. "-180.123456,-90.123456"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
del kwargs['max_length']
|
||||
return name, path, args, kwargs
|
||||
|
||||
@property
|
||||
def non_db_attrs(self):
|
||||
return super().non_db_attrs + ('options',) # type: ignore
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(**{
|
||||
'widget': MapLocationWidget({'options': self.options}),
|
||||
**kwargs
|
||||
})
|
||||
|
||||
def from_db_value(self, value: str, expression, connection) \
|
||||
-> 'Position|None':
|
||||
if not value:
|
||||
return None
|
||||
lat, lon = value.split(',')
|
||||
return Position(float(lat), float(lon))
|
||||
|
||||
def to_python(self, value) -> 'Position|None':
|
||||
# throw django.core.exceptions.ValidationError
|
||||
if isinstance(value, Position):
|
||||
return value
|
||||
if not value:
|
||||
return None
|
||||
lat, lon = value.split(',')
|
||||
return Position(float(lat), float(lon))
|
||||
|
||||
def get_prep_value(self, value) -> str:
|
||||
if isinstance(value, Position):
|
||||
return f'{value.lat},{value.long}'
|
||||
return value
|
||||
|
||||
def db_type(self, connection):
|
||||
return 'char(22)'
|
||||
BIN
map_location/static/leaflet/images/layers-2x.png
Normal file
BIN
map_location/static/leaflet/images/layers-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
map_location/static/leaflet/images/layers.png
Normal file
BIN
map_location/static/leaflet/images/layers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
map_location/static/leaflet/images/marker-icon-2x.png
Normal file
BIN
map_location/static/leaflet/images/marker-icon-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
map_location/static/leaflet/images/marker-icon.png
Normal file
BIN
map_location/static/leaflet/images/marker-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
map_location/static/leaflet/images/marker-shadow.png
Normal file
BIN
map_location/static/leaflet/images/marker-shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
661
map_location/static/leaflet/leaflet.css
Normal file
661
map_location/static/leaflet/leaflet.css
Normal file
@@ -0,0 +1,661 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
6
map_location/static/leaflet/leaflet.js
Normal file
6
map_location/static/leaflet/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
1
map_location/static/leaflet/leaflet.js.map
Normal file
1
map_location/static/leaflet/leaflet.js.map
Normal file
File diff suppressed because one or more lines are too long
63
map_location/static/leaflet/locate/L.Control.Locate.css
Executable file
63
map_location/static/leaflet/locate/L.Control.Locate.css
Executable file
@@ -0,0 +1,63 @@
|
||||
.leaflet-control-locate a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-control-locate a .leaflet-control-locate-location-arrow {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 7px;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="black" d="M445 4 29 195c-48 23-32 93 19 93h176v176c0 51 70 67 93 19L508 67c16-38-25-79-63-63z"/></svg>');
|
||||
}
|
||||
.leaflet-control-locate a .leaflet-control-locate-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 7px;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="black" d="M304 48a48 48 0 1 1-96 0 48 48 0 0 1 96 0zm-48 368a48 48 0 1 0 0 96 48 48 0 0 0 0-96zm208-208a48 48 0 1 0 0 96 48 48 0 0 0 0-96zM96 256a48 48 0 1 0-96 0 48 48 0 0 0 96 0zm13 99a48 48 0 1 0 0 96 48 48 0 0 0 0-96zm294 0a48 48 0 1 0 0 96 48 48 0 0 0 0-96zM109 61a48 48 0 1 0 0 96 48 48 0 0 0 0-96z"/></svg>');
|
||||
animation: leaflet-control-locate-spin 2s linear infinite;
|
||||
}
|
||||
.leaflet-control-locate.active a .leaflet-control-locate-location-arrow {
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="rgb(32, 116, 182)" d="M445 4 29 195c-48 23-32 93 19 93h176v176c0 51 70 67 93 19L508 67c16-38-25-79-63-63z"/></svg>');
|
||||
}
|
||||
.leaflet-control-locate.following a .leaflet-control-locate-location-arrow {
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="rgb(252, 132, 40)" d="M445 4 29 195c-48 23-32 93 19 93h176v176c0 51 70 67 93 19L508 67c16-38-25-79-63-63z"/></svg>');
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar .leaflet-locate-text-active {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar .leaflet-locate-text-active .leaflet-locate-icon {
|
||||
padding: 0 5px 0 0;
|
||||
}
|
||||
|
||||
.leaflet-control-locate-location circle {
|
||||
animation: leaflet-control-locate-throb 4s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes leaflet-control-locate-throb {
|
||||
0% {
|
||||
stroke-width: 1;
|
||||
}
|
||||
50% {
|
||||
stroke-width: 3;
|
||||
transform: scale(0.8, 0.8);
|
||||
}
|
||||
100% {
|
||||
stroke-width: 1;
|
||||
}
|
||||
}
|
||||
@keyframes leaflet-control-locate-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=L.Control.Locate.css.map */
|
||||
4
map_location/static/leaflet/locate/L.Control.Locate.min.js
vendored
Executable file
4
map_location/static/leaflet/locate/L.Control.Locate.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
map_location/static/leaflet/locate/L.Control.Locate.min.js.map
Executable file
1
map_location/static/leaflet/locate/L.Control.Locate.min.js.map
Executable file
File diff suppressed because one or more lines are too long
59
map_location/static/map-location.js
Normal file
59
map_location/static/map-location.js
Normal file
@@ -0,0 +1,59 @@
|
||||
function MapLocationInit(mapId, options = {}) {
|
||||
const valField = document.getElementById(mapId + '_value');
|
||||
const theMap = document.getElementById(mapId + '_map');
|
||||
const resetButton = document.getElementById(mapId + '_reset');
|
||||
|
||||
function isZero(latlng) {
|
||||
return latlng.lat === 0 && latlng.lng === 0;
|
||||
}
|
||||
|
||||
const osm = L.tileLayer(options.tileLayer || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', options.tileOptions);
|
||||
const map = L.map(mapId + '_map', {
|
||||
layers: [osm],
|
||||
center: [20, -25],
|
||||
zoom: 2,
|
||||
...(options.map || {})
|
||||
});
|
||||
L.control.locate({
|
||||
returnToPrevBounds: true,
|
||||
showPopup: false,
|
||||
}).addTo(map);
|
||||
|
||||
function loadPos() {
|
||||
if (!valField.value) {
|
||||
return [0, 0];
|
||||
} else {
|
||||
return valField.value.split(',');
|
||||
}
|
||||
}
|
||||
const marker = L.marker(loadPos(), { draggable: true }).addTo(map);
|
||||
marker.on('move', function (e) {
|
||||
const pos = map.wrapLatLng(e.latlng);
|
||||
const flag = isZero(pos);
|
||||
valField.value = flag ? null : pos.lat.toFixed(6) + ',' + pos.lng.toFixed(6);
|
||||
});
|
||||
map.on('click', function (e) {
|
||||
if (isZero(marker.getLatLng())) {
|
||||
marker.setLatLng(e.latlng);
|
||||
setMapState(false);
|
||||
}
|
||||
});
|
||||
resetButton.onclick = function () {
|
||||
marker.setLatLng([0, 0]);
|
||||
setMapState(true);
|
||||
return false;
|
||||
};
|
||||
|
||||
function setMapState(initial) {
|
||||
theMap.style.cursor = initial ? 'crosshair' : null;
|
||||
marker.setOpacity(initial ? 0 : 1);
|
||||
}
|
||||
|
||||
if (isZero(marker.getLatLng())) {
|
||||
setMapState(true);
|
||||
} else {
|
||||
map.setView(valField.value.split(','), options.markerZoom || 18);
|
||||
}
|
||||
// re-center map
|
||||
setTimeout(() => map.invalidateSize(), 300);
|
||||
}
|
||||
8
map_location/templates/forms/map-location.html
Normal file
8
map_location/templates/forms/map-location.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div style="width: 100%">
|
||||
<input name="{{ widget.name }}" id="{{ widget.attrs.id }}_value" type="hidden" value="{{ widget.value|default:'' }}" />
|
||||
<div id="{{ widget.attrs.id }}_map" style="width: 100%; height: 400px"></div>
|
||||
<a href="" id="{{ widget.attrs.id }}_reset">Remove</a>
|
||||
<script>
|
||||
MapLocationInit("{{ widget.attrs.id }}", {{ widget.attrs.options|safe }});
|
||||
</script>
|
||||
</div>
|
||||
BIN
screenshot.jpg
Normal file
BIN
screenshot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
33
setup.py
Normal file
33
setup.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import setuptools
|
||||
|
||||
with open('README.md', 'r') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setuptools.setup(
|
||||
name='django-map-location',
|
||||
description='Django Map-Location Field',
|
||||
version='0.9',
|
||||
author='relikd',
|
||||
license='MIT',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/relikd/django-map-location',
|
||||
packages=['map_location'],
|
||||
include_package_data=True,
|
||||
install_requires=['Django>=4.0'],
|
||||
keywords=['OpenStreetMap', 'OSM', 'Leaflet', 'Django',
|
||||
'Geo', 'GPS', 'Position', 'Location'],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 4.0',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
)
|
||||
Reference in New Issue
Block a user