├── books ├── __init__.py ├── utils │ ├── __init__.py │ ├── nearby.py │ └── filter.py ├── migrations │ ├── __init__.py │ ├── 0002_book_authors.py │ └── 0001_initial.py ├── apps.py ├── admin.py ├── urls.py ├── models.py ├── tests.py ├── templates │ └── books │ │ ├── viz.html │ │ ├── book_detail.html │ │ ├── new.html │ │ └── books.html ├── forms.py └── views.py ├── utils ├── __init__.py ├── delete_books.py ├── fakes.py ├── generate_data.py └── make_fakes.py ├── accounts ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── templates │ └── accounts │ │ ├── registration_form.html │ │ └── login_form.html ├── views.py └── forms.py ├── authors ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── apps.py ├── urls.py ├── models.py ├── views.py └── templates │ └── authors │ └── author_detail.html ├── djangoapp ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ └── prod.py ├── wsgi.py └── urls.py ├── functional_tests ├── __init__.py └── tests.py ├── db.sqlite3 ├── static ├── images │ ├── book.jpg │ ├── marker-icon.png │ └── marker-icon1.png ├── css │ ├── main.css │ ├── leaflet-search.css │ └── map-style.css ├── dist │ ├── MarkerCluster.css │ └── MarkerCluster.Default.css └── js │ ├── bundle.min.js │ └── leaflet-control.min.js ├── .gitignore ├── templates ├── footer.html ├── nav.html └── base.html ├── .travis.yml ├── requirements.txt ├── manage.py └── README.md /books/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /books/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djangoapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /books/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functional_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /authors/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /authors/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-leaflet-demo/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /authors/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /static/images/book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-leaflet-demo/HEAD/static/images/book.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | .vscode 3 | venv 4 | geckodriver.log 5 | do 6 | do* 7 | djangoapp/settings/local.py 8 | -------------------------------------------------------------------------------- /books/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BooksConfig(AppConfig): 5 | name = 'books' 6 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /authors/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthorsConfig(AppConfig): 5 | name = 'authors' 6 | -------------------------------------------------------------------------------- /static/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-leaflet-demo/HEAD/static/images/marker-icon.png -------------------------------------------------------------------------------- /static/images/marker-icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/django-leaflet-demo/HEAD/static/images/marker-icon1.png -------------------------------------------------------------------------------- /books/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Book 3 | # Register your models here. 4 | 5 | admin.site.register(Book) -------------------------------------------------------------------------------- /djangoapp/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | from .prod import * 4 | 5 | try: 6 | from .local import * 7 | except: 8 | pass -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from accounts.views import ( 3 | login_view, 4 | register_view, 5 | logout_view, 6 | ) 7 | 8 | app_name = "accounts" 9 | 10 | urlpatterns = [ 11 | url(r'^login/$', login_view, name="login"), 12 | url(r'^logout/$', logout_view, name="logout"), 13 | url(r'^register/$', register_view, name="register"), 14 | ] -------------------------------------------------------------------------------- /utils/delete_books.py: -------------------------------------------------------------------------------- 1 | from authors.models import Author 2 | from books.models import Book 3 | from django.contrib.auth.models import User 4 | 5 | authors = Author.objects.all() 6 | 7 | for author in authors: 8 | author.delete() 9 | 10 | books = Book.objects.all() 11 | 12 | for book in books: 13 | book.delete() 14 | 15 | users = User.objects.all() 16 | 17 | for user in users: 18 | user.delete() 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.5" 5 | 6 | services: 7 | - postgresql 8 | 9 | env: 10 | -DJANGO=2.0 DB=postgresql 11 | 12 | install: 13 | - pip install -r requirements.txt 14 | 15 | before_script: 16 | - psql -c "CREATE USER u_brian WITH PASSWORD 'Saintmary88'; ALTER USER u_brian CREATEDB;" -U postgres 17 | 18 | script: 19 | - python manage.py test books/ 20 | -------------------------------------------------------------------------------- /authors/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, re_path, include 3 | 4 | from . import views 5 | 6 | app_name = 'authors' 7 | 8 | urlpatterns = [ 9 | 10 | # re_path('^$', views.all_books, name="all"), 11 | # re_path('^new/$', views.new_book, name="new"), 12 | re_path('^search_authors/$', views.search_authors, name="search_authors"), 13 | path('/', views.author_detail, name="author_detail"), 14 | 15 | 16 | ] 17 | -------------------------------------------------------------------------------- /djangoapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djangoapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoapp.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bokeh==0.12.14 2 | dj-database-url==0.4.2 3 | dj-static==0.0.6 4 | Django==2.0.2 5 | django-crispy-forms==1.7.0 6 | django-toolbelt==0.0.1 7 | factory-boy==2.10.0 8 | Faker==0.8.11 9 | gunicorn==19.7.1 10 | Jinja2==2.10 11 | MarkupSafe==1.0 12 | numpy==1.14.1 13 | packaging==16.8 14 | psycopg2==2.7.4 15 | pyparsing==2.2.0 16 | python-dateutil==2.6.1 17 | pytz==2018.3 18 | PyYAML==3.12 19 | selenium==3.9.0 20 | six==1.11.0 21 | static3==0.7.0 22 | text-unidecode==1.1 23 | tornado==4.5.3 24 | xlwt==1.3.0 25 | -------------------------------------------------------------------------------- /books/migrations/0002_book_authors.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-22 02:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('authors', '0001_initial'), 10 | ('books', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='book', 16 | name='authors', 17 | field=models.ManyToManyField(to='authors.Author'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /books/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, re_path, include 3 | 4 | from . import views 5 | 6 | app_name = 'books' 7 | 8 | urlpatterns = [ 9 | 10 | re_path('^$', views.all_books, name="all"), 11 | re_path('^new/$', views.new_book, name="new"), 12 | re_path('^viz/$', views.viz, name="viz"), 13 | path('//', views.book_detail, name="book_detail"), 14 | re_path('^csv/$', views.export_filtered_books_csv, name="csv"), 15 | re_path('^xls/$', views.export_filtered_books_xls, name="xls"), 16 | ] 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoapp.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /authors/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | # from books.models import Book 3 | # Create your models here. 4 | class Author(models.Model): 5 | first_name = models.CharField(max_length=100) 6 | last_name = models.CharField(max_length=100) 7 | full_name = models.TextField() 8 | 9 | def __str__(self): 10 | return self.last_name + ", " + self.first_name 11 | 12 | def save(self, *args, **kwargs): 13 | self.full_name = self.first_name + " " + self.last_name 14 | super(Author, self).save(*args, **kwargs) 15 | 16 | # def authored_books(self): 17 | # books = Book.objects.filter() -------------------------------------------------------------------------------- /accounts/templates/accounts/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block head_extra %} 5 | {{ form.media }} 6 | {{ block.super }} 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Register

13 |
14 | {% csrf_token %} 15 | {{ form | crispy }} 16 |
or login to an existing account 17 |
18 |
19 |
20 | {% endblock %} -------------------------------------------------------------------------------- /authors/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-22 01:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Author', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('first_name', models.CharField(max_length=100)), 19 | ('last_name', models.CharField(max_length=100)), 20 | ('full_name', models.TextField()), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /utils/fakes.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import faker 3 | import random 4 | import factory 5 | from books.models import Book 6 | 7 | 8 | class BookFactory(factory.Factory): 9 | 10 | class Meta: 11 | model = 'books.Book' 12 | 13 | 14 | import factory 15 | import random 16 | from faker import Faker 17 | fake = Faker() 18 | title = factory.Faker('sentence', nb_words=4) 19 | lon = random.uniform(-180,180) 20 | lat = random.uniform(-90,90) 21 | pages = random.randint(20,2000) 22 | publish_date = fake.date_time_between(start_date="-10y", end_date="now", tzinfo=None) 23 | website = fake.url(schemes=None) 24 | 25 | def __str__(self): 26 | return self.title 27 | 28 | # b = BookFactory() 29 | # print(b) 30 | -------------------------------------------------------------------------------- /accounts/templates/accounts/login_form.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'base.html' %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block head_extra %} 6 | {{ form.media }} 7 | {{ block.super }} 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 | 13 |
14 |

Login

15 |
16 | {% csrf_token %} 17 | {{ form | crispy }} 18 | 19 | 22 |
or create an account 23 |
24 |
25 |
26 | {% endblock %} -------------------------------------------------------------------------------- /books/utils/nearby.py: -------------------------------------------------------------------------------- 1 | from math import radians, cos, sin, asin, sqrt 2 | 3 | def distance(origin, destination): 4 | """ 5 | Calculate the great circle distance between two points 6 | on the earth (specified in decimal degrees) 7 | """ 8 | 9 | lon1, lat1 = origin 10 | lon2, lat2 = destination 11 | # convert decimal degrees to radians 12 | lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) 13 | 14 | # haversine formula 15 | dlon = lon2 - lon1 16 | dlat = lat2 - lat1 17 | a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 18 | c = 2 * asin(sqrt(a)) 19 | r = 3956 # Radius of earth in kilometers. Use 3956 for miles 20 | return c * r 21 | 22 | # philly 39.9526° N, 75.1652° 23 | # DC 38.9072° N, 77.0369° W 24 | 25 | # 139 miles 223.699 km -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | /* body { 2 | padding-top: 20px; 3 | } */ 4 | 5 | h1 { 6 | color:red 7 | } 8 | 9 | .navbar-custom { 10 | background-color: #cad1e0; 11 | } 12 | 13 | .button-wrapper .btn { 14 | margin-bottom:5px; 15 | } 16 | 17 | .form-control { 18 | margin-bottom:5px; 19 | } 20 | 21 | .pull-right { 22 | float: right !important; 23 | } 24 | 25 | /* .Site { 26 | display: flex; 27 | min-height: 100vh; 28 | flex-direction: column; 29 | } 30 | 31 | .Site-content { 32 | flex: 1; 33 | } */ 34 | 35 | .footer { 36 | /* position: absolute; */ 37 | bottom: 0; 38 | width: 100%; 39 | height: 60px; /* Set the fixed height of the footer here */ 40 | line-height: 60px; /* Vertically center the text there */ 41 | background-color: #f5f5f5; 42 | } 43 | 44 | 45 | #mapid { 46 | height: 280px; 47 | width: auto; 48 | margin-bottom:5px; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /static/dist/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; 3 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; 4 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; 5 | transition: transform 0.3s ease-out, opacity 0.3s ease-in; 6 | } 7 | 8 | .leaflet-cluster-spider-leg { 9 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ 10 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; 11 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; 12 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; 13 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; 14 | } 15 | -------------------------------------------------------------------------------- /books/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.template.defaultfilters import slugify 3 | 4 | # Create your models here. 5 | 6 | class Book(models.Model): 7 | title = models.CharField(max_length=300) 8 | lat = models.DecimalField(max_digits=20, decimal_places=6) 9 | lon = models.DecimalField(max_digits=20, decimal_places=6) 10 | pages = models.IntegerField(default=1000) 11 | publish_date = models.DateField() 12 | website = models.URLField() 13 | synopsis = models.TextField() 14 | slug = models.SlugField(max_length=200) 15 | status = models.BooleanField(default=True) 16 | authors = models.ManyToManyField('authors.Author') 17 | 18 | def save(self, *args, **kwargs): 19 | self.slug = slugify(self.title) 20 | super(Book, self).save(*args, **kwargs) 21 | 22 | def __str__(self): 23 | return self.title 24 | 25 | def get_absolute_url(self): 26 | return '/books/{}/{}/'.format(self.id, self.slug) -------------------------------------------------------------------------------- /books/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from .models import Book 3 | from django.contrib.auth.models import User 4 | import datetime 5 | 6 | # Create your tests here. 7 | 8 | class SimpleTest(TestCase): 9 | def setUp(self): 10 | self.user = User.objects.create_user(username='testuser', password='12345') 11 | login = self.client.login(username='testuser', password='12345') 12 | 13 | def test_create_book(self): 14 | book = Book.objects.create( 15 | title="Test Book", 16 | lon=43.432, 17 | lat=34.234, 18 | publish_date=datetime.datetime.now(), 19 | pages=345, 20 | website="http://www.mybook.com", 21 | synopsis="This is the synopsis of the book.", 22 | status=True) 23 | book.save() 24 | all_books = Book.objects.all() 25 | self.assertEqual(len(all_books),1) 26 | first_book = all_books.first() 27 | self.assertIn("Test Book", first_book.title) -------------------------------------------------------------------------------- /authors/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse, HttpResponse 2 | from django.shortcuts import render 3 | from .models import Author 4 | 5 | import json 6 | 7 | # Create your views here. 8 | def author_detail(request, id): 9 | author = Author.objects.get(id=id) 10 | context = {'author':author} 11 | return render(request, 'authors/author_detail.html', context) 12 | 13 | def search_authors(request): 14 | if request.is_ajax(): 15 | print(request.GET) 16 | q = request.GET.get('term', '') 17 | authors = Author.objects.filter(full_name__icontains = q)[:10] 18 | results = [] 19 | for author in authors: 20 | author_json = {} 21 | author_json['id'] = author.full_name 22 | author_json['label'] = author.full_name 23 | author_json['value'] = author.full_name 24 | results.append(author_json) 25 | data = json.dumps(results) 26 | else: 27 | data = 'fail' 28 | mimetype = 'application/json' 29 | return HttpResponse(data, mimetype) -------------------------------------------------------------------------------- /djangoapp/urls.py: -------------------------------------------------------------------------------- 1 | """djangoapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, re_path, include 18 | 19 | urlpatterns = [ 20 | re_path('books/', include('books.urls', namespace="books")), 21 | re_path('authors/', include('authors.urls', namespace="authors")), 22 | path('admin/', admin.site.urls), 23 | path('accounts/', include('accounts.urls', namespace="accounts")), 24 | ] 25 | -------------------------------------------------------------------------------- /books/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-02-20 02:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Book', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=300)), 19 | ('lat', models.DecimalField(decimal_places=6, max_digits=20)), 20 | ('lon', models.DecimalField(decimal_places=6, max_digits=20)), 21 | ('pages', models.IntegerField(default=1000)), 22 | ('publish_date', models.DateField()), 23 | ('website', models.URLField()), 24 | ('synopsis', models.TextField()), 25 | ('slug', models.SlugField(max_length=200)), 26 | ('status', models.BooleanField(default=True)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functional_tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import LiveServerTestCase 2 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 3 | from selenium import webdriver 4 | import time 5 | import unittest 6 | 7 | class NewUserTest(StaticLiveServerTestCase): 8 | 9 | def setUp(self): 10 | self.b = webdriver.Firefox() 11 | self.domain = "http://127.0.0.1:8081" 12 | 13 | def tearDown(self): 14 | self.b.quit() 15 | super().tearDown() 16 | 17 | def test_register_new_user(self): 18 | self.b.get(self.domain + '/accounts/register') 19 | html = self.b.page_source 20 | self.assertIn('Register', html) 21 | self.b.find_element_by_id('id_username').send_keys('jim') 22 | self.b.find_element_by_id('id_email').send_keys('briancaffey2010@gmail.com') 23 | self.b.find_element_by_id('id_confirm_email').send_keys('briancaffey2010@gmail.com') 24 | self.b.find_element_by_id('id_first_name').send_keys('Brian') 25 | self.b.find_element_by_id('id_last_name').send_keys('Caffey') 26 | self.b.find_element_by_id('id_password').send_keys('qwer1234') 27 | self.b.find_element_by_id('id_submit').click() 28 | html = self.b.page_source 29 | # self.assertTemplateUsed('books.html') 30 | self.assertNotIn('Register', html) -------------------------------------------------------------------------------- /books/utils/filter.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from functools import reduce 3 | from ..models import Book 4 | import datetime 5 | 6 | def filter_books(books, paramDict): 7 | # paramDict = request.GET 8 | params = paramDict.keys() 9 | 10 | # data filtering 11 | if any(x!='' for x in paramDict.values()): 12 | if paramDict['publish_date_after'] != '': 13 | after_date = paramDict['publish_date_after'] 14 | _after_date = datetime.datetime.strptime(after_date, '%m/%d/%Y') 15 | 16 | books = books.filter(publish_date__gte=_after_date) 17 | 18 | if paramDict['publish_date_before'] != '': 19 | before_date = paramDict['publish_date_before'] 20 | _before_date = datetime.datetime.strptime(before_date, '%m/%d/%Y') 21 | books = books.filter(publish_date__lte=_before_date) 22 | 23 | # filters records that contain any of the following keywords 24 | if paramDict['keywords'] != '': 25 | kws = paramDict['keywords'].split() 26 | q_lookups = [Q(title__icontains=kw) for kw in kws] + \ 27 | [Q(synopsis__icontains=kw) for kw in kws] + \ 28 | [Q(website__icontains=kw) for kw in kws] 29 | filters = Q() 30 | filters |= reduce(lambda x, y: x | y, q_lookups) 31 | books = books.filter(filters) 32 | 33 | return books -------------------------------------------------------------------------------- /static/dist/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | /* IE 6-8 fallback colors */ 23 | .leaflet-oldie .marker-cluster-small { 24 | background-color: rgb(181, 226, 140); 25 | } 26 | .leaflet-oldie .marker-cluster-small div { 27 | background-color: rgb(110, 204, 57); 28 | } 29 | 30 | .leaflet-oldie .marker-cluster-medium { 31 | background-color: rgb(241, 211, 87); 32 | } 33 | .leaflet-oldie .marker-cluster-medium div { 34 | background-color: rgb(240, 194, 12); 35 | } 36 | 37 | .leaflet-oldie .marker-cluster-large { 38 | background-color: rgb(253, 156, 115); 39 | } 40 | .leaflet-oldie .marker-cluster-large div { 41 | background-color: rgb(241, 128, 23); 42 | } 43 | 44 | .marker-cluster { 45 | background-clip: padding-box; 46 | border-radius: 20px; 47 | } 48 | .marker-cluster div { 49 | width: 30px; 50 | height: 30px; 51 | margin-left: 5px; 52 | margin-top: 5px; 53 | 54 | text-align: center; 55 | border-radius: 15px; 56 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | } 58 | .marker-cluster span { 59 | line-height: 30px; 60 | } -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import ( 2 | authenticate, 3 | get_user_model, 4 | login, 5 | logout, 6 | ) 7 | from django.contrib.auth.decorators import login_required 8 | from django.http import HttpResponseRedirect 9 | from django.shortcuts import render, redirect 10 | 11 | from .forms import UserLoginForm, UserRegistrationForm 12 | 13 | # Create your views here. 14 | 15 | def login_view(request): 16 | next_redirect = request.GET.get('next') 17 | form = UserLoginForm(request.POST or None) 18 | if form.is_valid(): 19 | next_redirect = request.POST.get('next') 20 | username = form.cleaned_data.get('username') 21 | password = form.cleaned_data.get('password') 22 | user = authenticate(username=username, password=password) 23 | login(request, user) 24 | print(next_redirect) 25 | if next_redirect != 'None': 26 | return redirect(next_redirect) 27 | return redirect('books:all') 28 | context = {'form':form, 'next':next_redirect} 29 | return render(request, 'accounts/login_form.html', context) 30 | 31 | 32 | def register_view(request): 33 | form = UserRegistrationForm(request.POST or None) 34 | if form.is_valid(): 35 | user = form.save(commit=False) 36 | password = form.cleaned_data.get('password') 37 | user.set_password(password) 38 | user.save() 39 | new_user = authenticate(username=user.username, password=password) 40 | login(request, new_user) 41 | return redirect('books:all') 42 | 43 | context = {'form':form} 44 | return render(request, 'accounts/registration_form.html', context) 45 | 46 | @login_required 47 | def logout_view(request): 48 | logout(request) 49 | return redirect('books:all') -------------------------------------------------------------------------------- /utils/generate_data.py: -------------------------------------------------------------------------------- 1 | from books.models import Book 2 | from django.contrib.auth.models import User 3 | import datetime 4 | from faker import Faker 5 | 6 | 7 | 8 | 9 | 10 | 11 | data = [ 12 | {'title':'My first book', 13 | 'lon':41, 14 | 'lat':13, 15 | 'pages':230, 16 | 'publish_date':datetime.datetime(2005, 7, 14, 12, 30), 17 | 'website':'https://www.myfirstbook.com', 18 | 'synopsis':'This is a book about Django', 19 | 'slug':'this-is-a-book-about-django', 20 | 'status':True}, 21 | 22 | {'title':'My second book', 23 | 'lon':33, 24 | 'lat':12, 25 | 'pages':540, 26 | 'publish_date':datetime.datetime(2008, 7, 8, 12, 30), 27 | 'website':'https://www.myfirstbook.com', 28 | 'synopsis':'This is a second book also about Django', 29 | 'slug':'this-is-a-second-django-book', 30 | 'status':True}, 31 | 32 | {'title':'My third book', 33 | 'lon':37, 34 | 'lat':15, 35 | 'pages':344, 36 | 'publish_date':datetime.datetime(2018, 7, 8, 12, 30), 37 | 'website':'https://www.myfirstbook1.com', 38 | 'synopsis':'This is a third book also about Django', 39 | 'slug':'this-is-a-third-django-book', 40 | 'status':True}, 41 | ] 42 | 43 | 44 | User.objects.create_superuser(username='brian', password='qwer1234', email='briancaffey2010@gmail.com') 45 | 46 | for b in data: 47 | book = Book.objects.create( 48 | title=b['title'], 49 | lon=b['lon'], 50 | lat=b['lat'], 51 | pages=b['pages'], 52 | publish_date=b['publish_date'], 53 | website=b['website'], 54 | synopsis=b['synopsis'], 55 | slug=b['slug'], 56 | status=b['status'], 57 | ) 58 | book.save() 59 | 60 | -------------------------------------------------------------------------------- /authors/templates/authors/author_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block title%}Add a New Book | {{ block.super}}{% endblock %} 5 | {% block head_extra %} 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endblock head_extra %} 29 | 30 | {% block content %} 31 | 32 |
33 | 34 |

{{ author.full_name }}

35 | 36 |
    37 | {% for book in author.book_set.values %} 38 |
  • 39 | {{ book.title }} 40 |
  • 41 | {% endfor %} 42 |
43 | 44 |
45 | 46 | {% endblock %} -------------------------------------------------------------------------------- /books/templates/books/viz.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block title%}Add a New Book | {{ block.super}}{% endblock %} 5 | {% block head_extra %} 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% endblock head_extra %} 37 | 38 | {% block content %} 39 | 40 |
41 | 42 | {{ div | safe }} 43 | 44 |
45 | 46 | {% endblock %} 47 | 48 | {% block extra %} 49 | 50 | 51 | {{ script | safe }} 52 | 53 | {% endblock %} -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}Djangoapp{% endblock %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% block head_extra %} 27 | 28 | {% endblock %} 29 | 30 | 31 | {% block nav %}{% include 'nav.html' %}{% endblock nav %} 32 | 33 | 34 |
{% block content %}{% endblock%}
35 | 36 | {% block footer %}{% include 'footer.html' %}{% endblock footer %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% block extra %} 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /static/css/leaflet-search.css: -------------------------------------------------------------------------------- 1 | 2 | .leaflet-container .leaflet-control-search { 3 | position:relative; 4 | float:left; 5 | background:#fff; 6 | color:#1978cf; 7 | -moz-border-radius: 4px; 8 | -webkit-border-radius: 4px; 9 | border-radius: 4px; 10 | background-color: rgba(255, 255, 255, 0.8); 11 | z-index:1000; 12 | box-shadow: 0 1px 7px rgba(0,0,0,0.65); 13 | margin-left: 10px; 14 | margin-top: 10px; 15 | } 16 | .leaflet-control-search.search-exp {/*expanded*/ 17 | box-shadow: 0 1px 7px #999; 18 | background: #fff; 19 | } 20 | .leaflet-control-search .search-input { 21 | display:block; 22 | float:left; 23 | background: #fff; 24 | border:1px solid #666; 25 | border-radius:2px; 26 | height:18px; 27 | padding:0 18px 0 2px; 28 | margin:3px 0 3px 3px; 29 | } 30 | .leaflet-control-search.search-load .search-input { 31 | background: url('../images/loader.gif') no-repeat center right #fff; 32 | } 33 | .leaflet-control-search.search-load .search-cancel { 34 | visibility:hidden; 35 | } 36 | .leaflet-control-search .search-cancel { 37 | display:block; 38 | width:22px; 39 | height:18px; 40 | position:absolute; 41 | right:22px; 42 | margin:3px 0; 43 | background: url('../images/search-icon.png') no-repeat 0 -46px; 44 | text-decoration:none; 45 | filter: alpha(opacity=80); 46 | opacity: 0.8; 47 | } 48 | .leaflet-control-search .search-cancel:hover { 49 | filter: alpha(opacity=100); 50 | opacity: 1; 51 | } 52 | .leaflet-control-search .search-cancel span { 53 | display:none;/* comment for cancel button imageless */ 54 | font-size:18px; 55 | line-height:20px; 56 | color:#ccc; 57 | font-weight:bold; 58 | } 59 | .leaflet-control-search .search-cancel:hover span { 60 | color:#aaa; 61 | } 62 | .leaflet-control-search .search-button { 63 | display:block; 64 | float:left; 65 | width:26px; 66 | height:26px; 67 | background: url('../images/search-icon.png') no-repeat 2px 2px #fff; 68 | border-radius:4px; 69 | } 70 | .leaflet-control-search .search-button:hover { 71 | background: url('../images/search-icon.png') no-repeat 2px -22px #fafafa; 72 | } 73 | .leaflet-control-search .search-tooltip { 74 | position:absolute; 75 | top:100%; 76 | left:0; 77 | float:left; 78 | list-style: none; 79 | padding-left: 0; 80 | min-width:120px; 81 | max-height:122px; 82 | box-shadow: 1px 1px 6px rgba(0,0,0,0.4); 83 | background-color: rgba(0, 0, 0, 0.25); 84 | z-index:1010; 85 | overflow-y:auto; 86 | overflow-x:hidden; 87 | cursor: pointer; 88 | } 89 | .leaflet-control-search .search-tip { 90 | margin:2px; 91 | padding:2px 4px; 92 | display:block; 93 | color:black; 94 | background: #eee; 95 | border-radius:.25em; 96 | text-decoration:none; 97 | white-space:nowrap; 98 | vertical-align:center; 99 | } 100 | .leaflet-control-search .search-button:hover { 101 | background-color: #f4f4f4; 102 | } 103 | .leaflet-control-search .search-tip-select, 104 | .leaflet-control-search .search-tip:hover { 105 | background-color: #fff; 106 | } 107 | .leaflet-control-search .search-alert { 108 | cursor:pointer; 109 | clear:both; 110 | font-size:.75em; 111 | margin-bottom:5px; 112 | padding:0 .25em; 113 | color:#e00; 114 | font-weight:bold; 115 | border-radius:.25em; 116 | } 117 | 118 | 119 | < 1 min to Spreed -------------------------------------------------------------------------------- /books/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Book 3 | 4 | import datetime 5 | 6 | 7 | class QueryForm(forms.Form): 8 | 9 | publish_date_before = forms.DateField( 10 | label='', 11 | required=False, 12 | # initial = datetime.datetime.now(), 13 | widget = forms.TextInput( 14 | attrs={ 15 | 'class': 'form-control', 16 | 'id':'datepicker1', 17 | 'placeholder':'published before' 18 | })) 19 | 20 | publish_date_after = forms.DateField( 21 | label='', 22 | required=False, 23 | # initial = datetime.datetime.now(), 24 | widget = forms.TextInput( 25 | attrs={ 26 | 'class': 'form-control', 27 | 'id':'datepicker2', 28 | 'placeholder':'published after' 29 | })) 30 | 31 | keywords = forms.CharField( 32 | required=False, 33 | label='', 34 | widget=forms.TextInput( 35 | 36 | attrs={ 37 | 'class':'form-control', 38 | 'placeholder':'space-separated words matching title, synopsis, website or tags' 39 | })) 40 | 41 | 42 | 43 | 44 | class BookForm(forms.ModelForm): 45 | 46 | title = forms.CharField( 47 | required=True, 48 | label = "", 49 | widget=forms.TextInput( 50 | attrs={ 51 | 'class':'form-control', 52 | 'id':'id_title', 53 | 'placeholder': "book title", 54 | 'label':'' 55 | })) 56 | 57 | 58 | publish_date = forms.DateField( 59 | label='', 60 | # initial = datetime.datetime.now(), 61 | widget = forms.TextInput( 62 | attrs={ 63 | 'class': 'form-control', 64 | 'id':'datepicker', 65 | 'placeholder':'publish date' 66 | })) 67 | 68 | synopsis = forms.CharField( 69 | label="", 70 | widget=forms.Textarea( 71 | attrs={ 72 | 'class':'form-control', 73 | 'id':'id_content', 74 | 'rows':5, 75 | 'placeholder': "book synopsis", 76 | 'label':'' 77 | })) 78 | 79 | pages = forms.IntegerField( 80 | label = "", 81 | widget=forms.TextInput( 82 | attrs={ 83 | 'class':'form-control', 84 | 'placeholder':'number of pages', 85 | 'type':'number', 86 | 'id':'id_pages' 87 | } 88 | ) 89 | ) 90 | 91 | 92 | lon = forms.CharField( 93 | label="", 94 | widget=forms.TextInput( 95 | attrs={ 96 | 'class':'form-control', 97 | 'id':'id_lon', 98 | 'placeholder': "longitude", 99 | 'type':'text', 100 | } 101 | ) 102 | ) 103 | 104 | lat = forms.CharField( 105 | label="", 106 | widget=forms.TextInput( 107 | attrs={ 108 | 'class':'form-control', 109 | 'id':'id_lat', 110 | 'placeholder': "latitude", 111 | 'type':'text', 112 | } 113 | ) 114 | ) 115 | 116 | author = forms.CharField( 117 | label="", 118 | widget=forms.TextInput( 119 | attrs={ 120 | 'class':'form-control add-author-input', 121 | 'id':'id_author', 122 | 'placeholder':"author", 123 | 'type':'text', 124 | 'name':'author' 125 | } 126 | ) 127 | ) 128 | 129 | class Meta: 130 | model = Book 131 | fields = [ 132 | 'title', 133 | 'publish_date', 134 | 'synopsis', 135 | 'pages', 136 | 'lon', 137 | 'lat', 138 | ] -------------------------------------------------------------------------------- /djangoapp/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangoapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '$e6(=*_9%-65_%v!hvabj9r=@jscffb-@42f^szzip+-gd%5rv' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'books', 40 | 'crispy_forms', 41 | 'accounts', 42 | 'authors', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'djangoapp.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'djangoapp.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 82 | 'NAME': 'djangoapp_prod', 83 | 'USER': 'u_brian', 84 | 'PASSWORD': 'Saintmary88', 85 | 'HOST': 'localhost', 86 | 'PORT': '', 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 126 | 127 | STATICFILES_DIRS = [ 128 | os.path.join(BASE_DIR, "static"), 129 | ] 130 | 131 | STATIC_URL = '/static/' 132 | 133 | STATIC_ROOT = '/home/brian/static' 134 | -------------------------------------------------------------------------------- /djangoapp/settings/prod.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangoapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '$e6(=*_9%-65_%v!hvabj9r=@jscffb-@42f^szzip+-gd%5rv' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'books', 40 | 'crispy_forms', 41 | 'accounts', 42 | 'authors', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'djangoapp.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'djangoapp.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 82 | 'NAME': 'djangoapp_prod', 83 | 'USER': 'u_brian', 84 | 'PASSWORD': 'Saintmary88', 85 | 'HOST': 'localhost', 86 | 'PORT': '', 87 | } 88 | } 89 | 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 127 | 128 | STATICFILES_DIRS = [ 129 | os.path.join(BASE_DIR, "static"), 130 | ] 131 | 132 | STATIC_URL = '/static/' 133 | 134 | STATIC_ROOT = '/home/brian/static' 135 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from django.contrib.auth import ( 4 | authenticate, 5 | get_user_model, 6 | login, 7 | logout, 8 | ) 9 | 10 | User = get_user_model() 11 | 12 | class UserLoginForm(forms.Form): 13 | username = forms.CharField() 14 | username = forms.CharField( 15 | required=True, 16 | label = "", 17 | widget=forms.TextInput( 18 | attrs={ 19 | 'class':'form-control', 20 | 'id':'id_username', 21 | 'placeholder': "username", 22 | 'label':''})) 23 | 24 | password = forms.CharField( 25 | label = "", 26 | widget=forms.PasswordInput( 27 | attrs={ 28 | 'class':'form-control', 29 | 'id':'id_password', 30 | 'placeholder':'password', 31 | 'label':''})) 32 | 33 | 34 | def clean(self, *args, **kwargs): 35 | username = self.cleaned_data.get("username") 36 | password = self.cleaned_data.get("password") 37 | user = authenticate(username=username, password=password) 38 | user_qs = User.objects.filter(username=username) 39 | if user_qs.count() == 1: 40 | user = user_qs.first() 41 | if not user: 42 | raise forms.ValidationError("This user does not exist") 43 | if not user.check_password(password): 44 | raise forms.ValidationError("Incorrect password") 45 | 46 | if not user.is_active: 47 | raise forms.ValidationError("This user is no longer active") 48 | 49 | return super(UserLoginForm, self).clean(*args, **kwargs) 50 | 51 | class UserRegistrationForm(forms.ModelForm): 52 | 53 | 54 | username = forms.CharField( 55 | required=True, 56 | label = "", 57 | widget=forms.TextInput( 58 | attrs={ 59 | 'class':'form-control', 60 | 'id':'id_username', 61 | 'placeholder': "username", 62 | 'label':''})) 63 | 64 | 65 | email = forms.EmailField( 66 | label = "", 67 | widget=forms.TextInput( 68 | attrs={ 69 | 'class':'form-control', 70 | 'id':'id_email', 71 | 'placeholder': "email", 72 | 'label':''})) 73 | 74 | email2 = forms.EmailField( 75 | label = "", 76 | widget=forms.TextInput( 77 | attrs={ 78 | 'class':'form-control', 79 | 'id':'id_confirm_email', 80 | 'placeholder': "confirm email", 81 | 'label':''})) 82 | 83 | first_name = forms.CharField( 84 | label = "", 85 | widget=forms.TextInput( 86 | attrs={ 87 | 'class':'form-control', 88 | 'id':'id_first_name', 89 | 'placeholder': "first name", 90 | 'label':'' 91 | } 92 | )) 93 | 94 | last_name = forms.CharField( 95 | label = "", 96 | widget=forms.TextInput( 97 | attrs={ 98 | 'class':'form-control', 99 | 'id':'id_last_name', 100 | 'placeholder': "last name", 101 | 'label':'' 102 | } 103 | )) 104 | 105 | password = forms.CharField( 106 | label = "", 107 | widget=forms.PasswordInput( 108 | attrs={ 109 | 'class':'form-control', 110 | 'id':'id_password', 111 | 'placeholder':'password', 112 | 'label':''} 113 | )) 114 | 115 | 116 | class Meta: 117 | model = User 118 | fields = [ 119 | 'username', 120 | 'first_name', 121 | 'last_name', 122 | 'email', 123 | 'email2', 124 | 'password', 125 | ] 126 | 127 | def clean_email2(self): 128 | email = self.cleaned_data.get('email') 129 | email2 = self.cleaned_data.get('email2') 130 | if email != email2: 131 | 132 | raise forms.ValidationError("Emails must match") 133 | email_qs = User.objects.filter(email=email) 134 | if email_qs.exists(): 135 | raise forms.ValidationError("This email has already exists") 136 | return email -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/briancaffey/django-leaflet-demo.svg?branch=master)](https://travis-ci.org/briancaffey/django-leaflet-demo) 2 | 3 | ## Todo 4 | 5 | - Filter data ✓ 6 | - Q lookups ✓ 7 | - Export data buttons ✓ 8 | - User pages 9 | - Author model 10 | - Categories 11 | - DRF 12 | - Admin pages 13 | - Record approval 14 | - PEP8 ✓ 15 | 16 | ## About 17 | 18 | This project shows how to use Leaflet maps in a Django project. I use a simple `Book` model to illustrate different model fields and how they can be used across different parts of Django, such as views, forms, urls and templates. I will also be using additional models such as `Author`, `Publisher` and `Like` to illustrate various types of relations and how they can work together. 19 | 20 | ### Map Data 21 | 22 | The main goal of this project is to show how to plot items with geographical coordinates on a map using custom markers. Plotted items display additional information when clicked. 23 | 24 | ### Data Filtering 25 | 26 | Django forms is used to make a form that users can use to filter records. Filter options include: 27 | 28 | - Keywords contained in the title, synopsis or website. This uses Django's Q lookups 29 | - Date filtering using `bootstrap-datepicker` plugin 30 | 31 | Filtered records then show up on the map. Form options are persisted with the option to be quickly changed or completely reset. Summary statistics of the filtered data are also displayed. 32 | 33 | ### DataTables 34 | 35 | DataTables is used for tabular display of data. This allows for easy sorting and further search and filtering of the data. 36 | 37 | ### Exporting Data 38 | 39 | Data can be exported into a number of formats including XLS, CSV or PDF. 40 | 41 | ### Adding Records 42 | 43 | I use Django ModelForms to build a form that allows users to add records. Geographical coordinate data can be added directly to the form and the cooresponding map marker is updated immediately. Also, when the map in the form is clicked, the form coordinate inputs are updated with values of the clicked location's coordinates. 44 | 45 | ### Using Q Lookups to for searching multiple fields by mulitple keywords 46 | 47 | [idiom for searching multiple keywords](https://stackoverflow.com/questions/35126136/filter-multiple-keywords) 48 | 49 | ```python 50 | def all_books(request): 51 | """ 52 | Main view for books. request.GET parameters are used to filter books 53 | """ 54 | books = Book.objects.all() 55 | form = QueryForm(request.GET or None) 56 | paramDict = request.GET 57 | params = paramDict.keys() 58 | 59 | # data filtering 60 | if any(x!='' for x in request.GET.values()): 61 | 62 | [...other filters...] 63 | 64 | # this code returns records that contain all of the keywords 65 | if paramDict['keywords']: 66 | keywords = paramDict['keywords'].split() 67 | for kw in keywords: 68 | books = books.filter( 69 | Q(title__icontains=kw) 70 | ) 71 | 72 | # filters records that contain any of the following keywords 73 | if paramDict['keywords'] != '': 74 | kws = paramDict['keywords'].split() 75 | q_lookups = [Q(title__icontains=kw) for kw in kws] + \ 76 | [Q(synopsis__icontains=kw) for kw in kws] + \ 77 | [Q(website__icontains=kw) for kw in kws] 78 | filters = Q() 79 | filters |= reduce(lambda x, y: x | y, q_lookups) 80 | books = books.filter(filters) 81 | [...] 82 | ``` 83 | 84 | If we have to search for keywords over many fields, it might make sense to search a single field that stores concatenated values of each field we want to store. To accomplish this, we could add to the 85 | 86 | ## Inline Radio Buttons 87 | 88 | 89 | 90 | 91 | ```html 92 | {% for choice in form.status_ %} 93 | {{ choice.choice_label }} 94 | {{ choice.tag }} 95 | {% endfor %} 96 | ``` 97 | 98 | ### AJAX Map Load 99 | 100 | ```javascript 101 | $.ajax({ 102 | type: "GET", 103 | url: "/books/map_data", 104 | success: function(data){ 105 | var books = data["data"] 106 | var new_lat = books[0].loc[0] 107 | var new_lon = books[0].loc[1] 108 | mymap.setView([0, 0], 2); 109 | populateMap(books) 110 | } 111 | }); 112 | ``` 113 | 114 | ## Deploying on Digital Ocean 115 | 116 | Most of my projects are deployed with Heroku, but for this project I wanted to change things up and learn about deployment on another platform. For deploying on a DigitalOcean droplet, I used 117 | 118 | Here's an overview of how to deploy this app to Ditial Ocean. 119 | 120 | - Create a Droplet with Ubunutu 14.04 121 | - Generate SSH Key 122 | - Login via SSH 123 | 124 | 125 | -------------------------------------------------------------------------------- /books/templates/books/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block title%}Add a New Book | {{ block.super}}{% endblock %} 5 | {% block head_extra %} 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endblock head_extra %} 29 | 30 | {% block content %} 31 | 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |

{{ book.title }} {{ book.pages }} pages

40 |

Authors: {% for author in book.authors.all %}{{ author.full_name }}{% if forloop.last %}{% else %}, {% endif %}{% endfor %}

41 |

{{ book.synopsis | safe | linebreaks }}

42 |
43 |
44 |
45 |
46 |
47 |

Other Books Nearby

48 | {% for b in sorted_nearby %} 49 |

50 | {{ b.1.1.1 }} {{ b.1.0 | floatformat:2 }} miles away 51 |

52 | {% endfor %} 53 |
54 |
55 |
56 |
57 |
58 | 59 |
60 | 102 |
103 | 104 | {% endblock %} 105 | 106 | -------------------------------------------------------------------------------- /utils/make_fakes.py: -------------------------------------------------------------------------------- 1 | from authors.models import Author 2 | from books.models import Book 3 | from django.contrib.auth.models import User 4 | import datetime 5 | from faker import Faker 6 | import json 7 | import random 8 | from django.template.defaultfilters import slugify 9 | 10 | User.objects.create_superuser(username='brian', password='qwer1234', email='briancaffey2010@gmail.com') 11 | 12 | 13 | fake = Faker() 14 | 15 | for i in range(100): 16 | name = fake.name().split() 17 | first_name = name[0] 18 | last_name = name[-1] 19 | author = Author.objects.create( 20 | first_name=first_name, 21 | last_name=last_name) 22 | author.save() 23 | 24 | 25 | with open('utils/cities.json') as data_file: 26 | data = json.load(data_file) 27 | 28 | 29 | def make_synopsis(n): 30 | from faker import Faker 31 | import random 32 | fake = Faker() 33 | synopsis = "" 34 | for i in range(n): 35 | synopsis += " ".join(fake.words(random.randint(10,20))).capitalize() + ". " 36 | return synopsis 37 | 38 | all_authors = Author.objects.all() 39 | 40 | for i in range(100): 41 | import random 42 | from faker import Faker 43 | fake = Faker() 44 | title = " ".join([i.capitalize() for i in fake.words(3)]) 45 | location = random.choice(data) 46 | lon = location['longitude'] 47 | lon = float(str.format('{0:.6f}', float(lon))) 48 | lat = location['latitude'] 49 | lat = float(str.format('{0:.6f}', float(lat))) 50 | pages = random.randint(100,3000) 51 | publish_date = fake.future_date() 52 | website = fake.url() 53 | slug = slugify(title) 54 | synopsis = make_synopsis(random.randint(4,7)) 55 | status = True 56 | 57 | book = Book.objects.create( 58 | title=title, 59 | lon=lat, 60 | lat=lon, 61 | pages=pages, 62 | publish_date=publish_date, 63 | website=website, 64 | slug=slug, 65 | synopsis=synopsis, 66 | status=status 67 | ) 68 | 69 | random_author = random.choice(all_authors) 70 | print(random_author) 71 | book.authors.add(random_author) 72 | book.save() 73 | # print(book.title) 74 | # print(book.authors.all()) 75 | # print(book.lon, book.lat) 76 | 77 | 78 | 79 | 80 | 81 | 82 | # {'title':'My third book', 83 | # 'lon':37, 84 | # 'lat':15, 85 | # 'pages':344, 86 | # 'publish_date':datetime.datetime(2018, 7, 8, 12, 30), 87 | # 'website':'https://www.myfirstbook1.com', 88 | # 'synopsis':'This is a third book also about Django', 89 | # 'slug':'this-is-a-third-django-book', 90 | # 'status':True}, 91 | 92 | # ['_Generator__config', '_Generator__format_token', '_Generator__random', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'add_provider', 'address', 'am_pm', 'ascii_company_email', 'ascii_email', 'ascii_free_email', 'ascii_safe_email', 'bank_country', 'bban', 'binary', 'boolean', 'bothify', 'bs', 'building_number', 'catch_phrase', 'century', 'chrome', 'city', 'city_prefix', 'city_suffix', 'color_name', 'company', 'company_email', 'company_suffix', 'country', 'country_code', 'credit_card_expire', 'credit_card_full', 'credit_card_number', 'credit_card_provider', 'credit_card_security_code', 'cryptocurrency_code', 'currency_code', 'date', 'date_between', 'date_between_dates', 'date_object', 'date_this_century', 'date_this_decade', 'date_this_month', 'date_this_year', 'date_time', 'date_time_ad', 'date_time_between', 'date_time_between_dates', 'date_time_this_century', 'date_time_this_decade', 'date_time_this_month', 'date_time_this_year', 'day_of_month', 'day_of_week', 'domain_name', 'domain_word', 'ean', 'ean13', 'ean8', 'email', 'file_extension', 'file_name', 'file_path', 'firefox', 'first_name', 'first_name_female', 'first_name_male', 'format', 'free_email', 'free_email_domain', 'future_date', 'future_datetime', 'geo_coordinate', 'get_formatter', 'get_providers', 'hex_color', 'iban', 'image_url', 'internet_explorer', 'ipv4', 'ipv6', 'isbn10', 'isbn13', 'iso8601', 'job', 'language_code', 'last_name', 'last_name_female', 'last_name_male', 'latitude', 'lexify', 'license_plate', 'linux_platform_token', 'linux_processor', 'locale', 'longitude', 'mac_address', 'mac_platform_token', 'mac_processor', 'md5', 'military_apo', 'military_dpo', 'military_ship', 'military_state', 'mime_type', 'month', 'month_name', 'msisdn', 'name', 'name_female', 'name_male', 'null_boolean', 'numerify', 'opera', 'paragraph', 'paragraphs', 'parse', 'password', 'past_date', 'past_datetime', 'phone_number', 'postalcode', 'postalcode_plus4', 'postcode', 'prefix', 'prefix_female', 'prefix_male', 'profile', 'provider', 'providers', 'pybool', 'pydecimal', 'pydict', 'pyfloat', 'pyint', 'pyiterable', 'pylist', 'pyset', 'pystr', 'pystruct', 'pytuple', 'random', 'random_digit', 'random_digit_not_null', 'random_digit_not_null_or_empty', 'random_digit_or_empty', 'random_element', 'random_int', 'random_letter', 'random_number', 'random_sample', 'random_sample_unique', 'randomize_nb_elements', 'rgb_color', 'rgb_css_color', 'safari', 'safe_color_name', 'safe_email', 'safe_hex_color', 'secondary_address', 'seed', 'seed_instance', 'sentence', 'sentences', 'set_formatter', 'sha1', 'sha256', 'simple_profile', 'slug', 'ssn', 'state', 'state_abbr', 'street_address', 'street_name', 'street_suffix', 'suffix', 'suffix_female', 'suffix_male', 'text', 'time', 'time_delta', 'time_object', 'time_series', 'timezone', 'tld', 'unix_time', 'uri', 'uri_extension', 'uri_page', 'uri_path', 'url', 'user_agent', 'user_name', 'uuid4', 'windows_platform_token', 'word', 'words', 'year', 'zipcode', 'zipcode_plus4'] 93 | -------------------------------------------------------------------------------- /books/views.py: -------------------------------------------------------------------------------- 1 | from authors.models import Author 2 | 3 | from bokeh.plotting import figure, output_file, show 4 | from bokeh.embed import components 5 | 6 | from django.contrib.auth.decorators import login_required 7 | from django.db.models import Q 8 | from django.db.models import Sum 9 | from django.http import HttpResponse, JsonResponse 10 | from django.shortcuts import redirect, render, render_to_response 11 | from django.utils.safestring import mark_safe 12 | from django.utils.html import escapejs 13 | from functools import reduce 14 | from .forms import BookForm, QueryForm 15 | from .models import Book 16 | from .utils.filter import filter_books 17 | from .utils.nearby import distance 18 | 19 | import csv 20 | import datetime 21 | import json 22 | import operator 23 | import xlwt 24 | # Create your views here. 25 | 26 | def all_books(request): 27 | """ 28 | Main view for books. request.GET parameters are used to filter books 29 | """ 30 | books = Book.objects.all() 31 | form = QueryForm(request.GET or None) 32 | paramDict = request.GET 33 | 34 | books = filter_books(books, paramDict) 35 | 36 | page_count = books.aggregate(Sum('pages')) 37 | 38 | map_books = [{'loc':[float(book.lon), float(book.lat)], 39 | 'title':book.title, 40 | 'url':book.get_absolute_url()} for book in books] 41 | context = { 42 | 'books':books, 43 | 'map_books': mark_safe(escapejs(json.dumps(map_books))), 44 | 'page_count':page_count['pages__sum'], 45 | 'form':form} 46 | return render(request, 'books/books.html', context) 47 | 48 | def export_filtered_books_csv(request): 49 | response = HttpResponse(content_type='text/csv') 50 | response['Content-Disposition'] = 'attachment; filename="books.csv"' 51 | 52 | writer = csv.writer(response) 53 | writer.writerow(['Title', 'Synopsis', 'Pages']) 54 | books = Book.objects.all() 55 | paramDict = request.GET 56 | print(paramDict) 57 | books = filter_books(books, paramDict) 58 | books = books.values_list( 59 | 'title', 60 | 'synopsis', 61 | 'pages') 62 | 63 | for book in books: 64 | writer.writerow(book) 65 | 66 | return response 67 | 68 | def export_filtered_books_xls(request): 69 | response = HttpResponse(content_type='application/ms-excel') 70 | response['Content-Disposition'] = 'attachment; filename="books.xls"' 71 | 72 | wb = xlwt.Workbook(encoding='utf-8') 73 | ws = wb.add_sheet('Books') 74 | 75 | # Sheet header, first row 76 | row_num = 0 77 | 78 | font_style = xlwt.XFStyle() 79 | font_style.font.bold = True 80 | 81 | columns = ['Books', 'Synopsis', 'Pages'] 82 | 83 | for col_num in range(len(columns)): 84 | ws.write(row_num, col_num, columns[col_num], font_style) 85 | 86 | # Sheet body, remaining rows 87 | font_style = xlwt.XFStyle() 88 | books = Book.objects.all() 89 | paramDict = request.GET 90 | books = filter_books(books, paramDict) 91 | rows = books.values_list('title', 'synopsis', 'pages') 92 | for row in rows: 93 | row_num += 1 94 | for col_num in range(len(row)): 95 | ws.write(row_num, col_num, row[col_num], font_style) 96 | 97 | wb.save(response) 98 | return response 99 | 100 | def book_detail(request, id, slug): 101 | 102 | book = Book.objects.get(id=id, slug=slug) 103 | b_coords = (book.lat, book.lon) 104 | all_books = Book.objects.all() 105 | coords = [((b.lat, b.lon),b) for b in all_books] 106 | 107 | distance_dict = {} 108 | for c in coords: 109 | if c[0] != b_coords: 110 | distance_dict[c[0]]=(distance(c[0],b_coords),c) 111 | 112 | sorted_nearby = sorted(distance_dict.items(), key=lambda x: x[1][0])[:5] 113 | 114 | map_book = [{'loc':[float(book.lon), float(book.lat)], 115 | 'title':book.title, 116 | 'url':book.get_absolute_url()}] 117 | context = { 118 | 'book':book, 119 | 'map_book':mark_safe(escapejs(json.dumps(map_book))), 120 | 'sorted_nearby':sorted_nearby, 121 | } 122 | return render(request, 'books/book_detail.html', context) 123 | 124 | @login_required 125 | def new_book(request): 126 | form = BookForm(None) 127 | if request.method == "POST": 128 | form = BookForm(request.POST) 129 | if form.is_valid(): 130 | title = form.cleaned_data['title'] 131 | lon = form.cleaned_data['lon'] 132 | lon = float(str.format('{0:.6f}', float(lon))) 133 | lat = form.cleaned_data['lat'] 134 | lat = float(str.format('{0:.6f}', float(lat))) 135 | publish_date = form.cleaned_data['publish_date'] 136 | pages = form.cleaned_data['pages'] 137 | synopsis = form.cleaned_data['synopsis'] 138 | authors = request.POST.getlist('author') 139 | print(authors) 140 | book = Book( 141 | title=title, 142 | lon=lon, 143 | lat=lat, 144 | synopsis=synopsis, 145 | pages=pages, 146 | publish_date=publish_date) 147 | book.save() 148 | for author in authors: 149 | if len(author.split()) > 1: 150 | first_name = "".join(author.split()[:-1]) 151 | last_name = author.split()[-1] 152 | a, created = Author.objects.get_or_create( 153 | first_name=first_name, 154 | last_name=last_name, 155 | ) 156 | book.authors.add(a) 157 | book.save() 158 | return redirect('books:all') 159 | return render(request, 'books/new.html', {'form':form}) 160 | 161 | def viz(request): 162 | x= [1,3,5,7,9,11,13] 163 | y= [1,2,3,4,5,6,7] 164 | title = 'y = f(x)' 165 | 166 | plot = figure(title= title , 167 | x_axis_label= 'X-Axis', 168 | y_axis_label= 'Y-Axis', 169 | plot_width =400, 170 | plot_height =400) 171 | 172 | plot.line(x, y, legend= 'f(x)', line_width = 2) 173 | #Store components 174 | script, div = components(plot) 175 | 176 | #Feed them to the Django template. 177 | return render_to_response( 'books/viz.html', 178 | {'script' : script , 'div' : div} ) -------------------------------------------------------------------------------- /books/templates/books/new.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block title%}Add a New Book | {{ block.super}}{% endblock %} 5 | {% block head_extra %} 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endblock head_extra %} 33 | 34 | {% block content %} 35 | 36 |
37 |

New Book

38 |

Add a new book here:

39 | {% for key, value in form.errors.items %} 40 |
{{ key }}: {{ value }}
41 | {% endfor %} 42 |
43 | {% csrf_token %} 44 | 45 |
46 |
47 | {{ form.title }} 48 |
49 | 50 |
51 | {{ form.publish_date }} 52 |
53 |
54 | 55 |
56 | {{ form.synopsis }} 57 |
58 | {{ form.pages }} 59 |
60 |
61 |
62 | {{ form.lon }} 63 |
64 |
65 | {{ form.lat }} 66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 | {{ form.author }} 74 |
75 |
76 | Add another author 77 |
78 |
79 | 80 | 119 | 120 | 180 | 181 | 182 |
183 |
184 |
185 | 186 | 191 | 192 | {% endblock %} -------------------------------------------------------------------------------- /static/css/map-style.css: -------------------------------------------------------------------------------- 1 | .Code__code___31Vg8 { 2 | position: absolute; 3 | top: 110px; 4 | right: 10px; 5 | bottom: 10px; 6 | width: 425px; 7 | 8 | background: rgba(0, 0, 0, .7); 9 | color: #fff; 10 | border-radius: 4px; 11 | padding: 24px; 12 | font-family: Roboto, monospace; 13 | white-space: pre; 14 | 15 | font-size: 14px; 16 | line-height: 22px; 17 | } 18 | .Layout__header___33oX6 { 19 | position: relative; 20 | width: 100%; 21 | height: 100px; 22 | padding: 0 0 0 20px; 23 | } 24 | 25 | .Layout__header___33oX6 h1 { 26 | font-family: 'Roboto', sans-serif; 27 | font-size: 32px; 28 | font-weight: 300; 29 | line-height: 64px; 30 | margin: 0; 31 | padding: 0; 32 | } 33 | 34 | .Layout__header___33oX6 ul { 35 | list-style: none; 36 | display: block; 37 | overflow: hidden; 38 | margin: 0; 39 | padding: 0; 40 | line-height: 32px 41 | } 42 | 43 | .Layout__header___33oX6 ul li { 44 | float: left; 45 | display: block; 46 | cursor: pointer; 47 | } 48 | 49 | .Layout__header___33oX6 ul li:hover { 50 | border-bottom: 4px solid #00bcd4; 51 | } 52 | 53 | .Layout__header___33oX6 ul li.active { 54 | border-bottom: 4px solid #2196f3; 55 | } 56 | 57 | .Layout__header___33oX6 ul li a { 58 | text-decoration: none; 59 | color: inherit; 60 | display: block; 61 | width: 100%; 62 | height: 100%; 63 | padding: 0 24px; 64 | } 65 | 66 | .Layout__content___GDRpI { 67 | position: absolute; 68 | top: 100px; 69 | bottom: 0; 70 | left: 20px; 71 | right: 465px; 72 | } 73 | 74 | .Layout__fullWidth___2Qlah { 75 | left: 0; 76 | right: 0; 77 | } 78 | .Map__map___2UbOE { 79 | position: absolute; 80 | display: block; 81 | width: 100%; 82 | height: 100%; 83 | } 84 | .SearchResults__item___3yUT- > * { 85 | border: 1px solid transparent; 86 | line-height: 32px; 87 | padding: 0 18px; 88 | 89 | white-space: nowrap; 90 | overflow: hidden; 91 | text-overflow: ellipsis; 92 | } 93 | 94 | .SearchResults__item___3yUT- > *:hover, 95 | .SearchResults__item___3yUT- > .active { 96 | background-color: #f8f8f8; 97 | border-color: #c6c6c6; 98 | } 99 | .Search__search___2kQjw form { 100 | position: relative; 101 | margin: 32px 0; 102 | background-color: #fff; 103 | vertical-align: top; 104 | border-radius: 2px; 105 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); 106 | transition: box-shadow 200ms cubic-bezier(0.4, 0.0, 0.2, 1); 107 | } 108 | 109 | .Search__search___2kQjw form:hover, 110 | .Search__search___2kQjw.active form { 111 | box-shadow: 0 3px 8px 0 rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.08); 112 | } 113 | 114 | .Search__search___2kQjw input { 115 | border: none; 116 | padding: 0; 117 | margin: 0; 118 | width: 100%; 119 | outline: none; 120 | font: 16px arial, sans-serif; 121 | line-height: 48px; 122 | height: 48px; 123 | text-indent: 18px; 124 | } 125 | 126 | html, body { 127 | font-family: 'Open Sans', sans-serif; 128 | margin: 0; 129 | padding: 0; 130 | height: 100%; 131 | width: 100%; 132 | box-sizing: border-box; 133 | } 134 | 135 | *, *:before, *:after { 136 | box-sizing: border-box; 137 | } 138 | 139 | .leaflet-control-geosearch.bar { 140 | position: absolute !important; 141 | left: 50px; 142 | right: 515px; 143 | } 144 | /* global styling */ 145 | .leaflet__leaflet-control-geosearch___35AKI *, 146 | .leaflet__leaflet-control-geosearch___35AKI *:before, 147 | .leaflet__leaflet-control-geosearch___35AKI *:after { 148 | box-sizing: border-box; 149 | } 150 | 151 | /* leaflet button styling */ 152 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__leaflet-bar-part___2_DBQ { 153 | border-radius: 4px; 154 | border-bottom: none; 155 | } 156 | 157 | .leaflet__leaflet-control-geosearch___35AKI a.leaflet__leaflet-bar-part___2_DBQ:before, 158 | .leaflet__leaflet-control-geosearch___35AKI a.leaflet__leaflet-bar-part___2_DBQ:after { 159 | position: absolute; 160 | display: block; 161 | content: ''; 162 | } 163 | 164 | /* magnifying glass */ 165 | .leaflet__leaflet-control-geosearch___35AKI a.leaflet__leaflet-bar-part___2_DBQ:before { 166 | top: 19px; 167 | left: 16px; 168 | width: 8px; 169 | border-top: 2px solid #555; 170 | transform: rotateZ(45deg); 171 | } 172 | 173 | .leaflet__leaflet-control-geosearch___35AKI a.leaflet__leaflet-bar-part___2_DBQ:after { 174 | top: 6px; 175 | left: 6px; 176 | height: 14px; 177 | width: 14px; 178 | border-radius: 50%; 179 | border: 2px solid #555; 180 | } 181 | 182 | /* resets for pending and error icons */ 183 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__error___15pc6 a.leaflet__leaflet-bar-part___2_DBQ:before, 184 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__pending___3RDwM a.leaflet__leaflet-bar-part___2_DBQ:before { 185 | display: none; 186 | } 187 | 188 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__pending___3RDwM a.leaflet__leaflet-bar-part___2_DBQ:after, 189 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__error___15pc6 a.leaflet__leaflet-bar-part___2_DBQ:after { 190 | left: 50%; 191 | top: 50%; 192 | width: 18px; 193 | height: 18px; 194 | margin: -9px 0 0 -9px; 195 | border-radius: 50%; 196 | } 197 | 198 | /* pending icon */ 199 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__pending___3RDwM a.leaflet__leaflet-bar-part___2_DBQ:after { 200 | content: ''; 201 | border: 2px solid #555; 202 | border-top: 2px solid #f3f3f3; 203 | animation: leaflet__spin___TqeCo 1s linear infinite; 204 | } 205 | 206 | /* error icon */ 207 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__error___15pc6 a.leaflet__leaflet-bar-part___2_DBQ:after { 208 | content: '!'; 209 | line-height: initial; 210 | font-weight: 600; 211 | font-size: 18px; 212 | border: none; 213 | } 214 | 215 | /* search form styling */ 216 | .leaflet__leaflet-control-geosearch___35AKI form { 217 | display: none; 218 | position: absolute; 219 | top: -2px; 220 | left: 28px; 221 | border-radius: 0 4px 4px 0; 222 | border: 2px solid rgba(0, 0, 0, 0.2); 223 | border-left: none; 224 | background-color: #fff; 225 | background-clip: padding-box; 226 | z-index: -1; 227 | height: auto; 228 | margin: 0; 229 | padding: 0 8px; 230 | } 231 | 232 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__active___WG2p7 form { 233 | display: block; 234 | } 235 | 236 | .leaflet__leaflet-control-geosearch___35AKI form input { 237 | min-width: 200px; 238 | width: 100%; 239 | border: none; 240 | outline: none; 241 | margin: 0; 242 | padding: 0; 243 | font-size: 12px; 244 | height: 30px; 245 | border-radius: 0 4px 4px 0; 246 | text-indent: 8px; 247 | } 248 | 249 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__results___19EcW { 250 | background: #fff; 251 | } 252 | 253 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__results___19EcW > * { 254 | line-height: 24px; 255 | padding: 0 8px; 256 | border: 1px solid transparent; 257 | 258 | white-space: nowrap; 259 | overflow: hidden; 260 | text-overflow: ellipsis; 261 | } 262 | 263 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__results___19EcW.leaflet__active___WG2p7 { 264 | padding: 8px 0; 265 | border-top: 1px solid #c6c6c6; 266 | } 267 | 268 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__results___19EcW > .leaflet__active___WG2p7, 269 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__results___19EcW > :hover { 270 | background-color: #f8f8f8; 271 | border-color: #c6c6c6; 272 | cursor: pointer; 273 | } 274 | 275 | /* add missing border to form */ 276 | .leaflet__leaflet-control-geosearch___35AKI .leaflet__results___19EcW.leaflet__active___WG2p7:after { 277 | content: ''; 278 | display: block; 279 | width: 0; 280 | border-left: 2px solid rgba(0, 0, 0, .2); 281 | position: absolute; 282 | left: -2px; 283 | bottom: -2px; 284 | top: 30px; 285 | } 286 | 287 | /* animations */ 288 | @keyframes leaflet__spin___TqeCo { 289 | 0% { transform: rotate(0deg); } 290 | 100% { transform: rotate(360deg); } 291 | } 292 | 293 | .leaflet__leaflet-top___31xh0 .leaflet__leaflet-control-geosearch___35AKI.leaflet__bar___fXvvA, 294 | .leaflet__leaflet-bottom___OI2at .leaflet__leaflet-control-geosearch___35AKI.leaflet__bar___fXvvA { 295 | display: none; 296 | } 297 | 298 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__bar___fXvvA { 299 | position: relative; 300 | display: block; 301 | height: auto; 302 | width: 400px; 303 | margin: 10px auto 0; 304 | cursor: auto; 305 | z-index: 1000; 306 | } 307 | 308 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__bar___fXvvA form { 309 | position: relative; 310 | top: 0; 311 | left: 0; 312 | display: block; 313 | border: 2px solid rgba(0, 0, 0, 0.2); 314 | border-radius: 4px; 315 | } 316 | 317 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__bar___fXvvA form input { 318 | min-width: 100%; 319 | width: 100%; 320 | } 321 | 322 | .leaflet__leaflet-control-geosearch___35AKI.leaflet__bar___fXvvA .leaflet__results___19EcW.leaflet__active___WG2p7:after { 323 | opacity: .2; 324 | } 325 | 326 | .leaflet__leaflet-right___3WKWY .leaflet__leaflet-control-geosearch___35AKI form { 327 | right: 28px; 328 | left: initial; 329 | border-radius: 4px 0 0 4px; 330 | border-left: inherit; 331 | border-right: none; 332 | } 333 | 334 | .leaflet__leaflet-control-geosearch___35AKI a.leaflet__reset___1fyCm { 335 | color: black; 336 | position: absolute; 337 | line-height: 30px; 338 | padding: 0 8px; 339 | right: 0; 340 | top: 0; 341 | cursor: pointer; 342 | border: none; 343 | } 344 | 345 | .leaflet__leaflet-control-geosearch___35AKI a.leaflet__reset___1fyCm:hover { 346 | background: #f5f5f5; 347 | } -------------------------------------------------------------------------------- /books/templates/books/books.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block title%}Books | {{ block.super}}{% endblock %} 5 | {% block head_extra %} 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% endblock head_extra %} 39 | 40 | {% block content %} 41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |

52 | 53 |
54 | 55 | 58 | 59 | 60 |
61 |

62 |
63 |
64 | {{ books.count }} book{{ books | pluralize }} - {{ page_count }} page{{ page_count | pluralize }} 65 |
66 |
67 | 68 |
69 |
70 |

Use this form to filter books by different attributes

71 | 72 |

73 | 74 |

75 |

76 | {{ form.keywords }} 77 |

78 |
79 |
80 | {{ form.publish_date_before }} 81 |
82 |
83 | {{ form.publish_date_after }} 84 |
85 | 86 |
87 | 88 | 89 |

90 |

91 | 92 | 93 | 94 |

95 | 96 |
97 | 126 | 127 |

128 | 129 |
130 |
131 |
132 |
133 | 134 | 135 |
136 | 137 | 196 |
197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | {% for book in books %} 216 | 217 | 218 | 219 | 220 | 221 | 222 | {% endfor %} 223 | 224 |
TitlePagesPublish DateWebsite
TitlePagesPublish DateWebsite
{{ book.title }}{{ book.pages }}{{ book.publish_date }}{{ book.website }}
225 |
226 | 234 | 235 |
236 | 237 | 243 | 244 | {% endblock %} 245 | 246 | -------------------------------------------------------------------------------- /static/js/bundle.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.GeoSearch=t():e.GeoSearch=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){n(2),e.exports=n(7)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=n(2),a=(r(i),function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}()),u=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};o(this,e),this.options=t}return a(e,[{key:"getParamString",value:function(e){return Object.keys(e).map(function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])}).join("&")}},{key:"search",value:function(e){return new Promise(function(t,n){var r,o,i,a,u;return r=e.query,o=~location.protocol.indexOf("http")?location.protocol:"https:",i=this.endpoint({query:r,protocol:o}),fetch(i).then(function(e){return a=e,a.json().then(function(e){return u=e,t(this.parse({data:u}))}.$asyncbind(this,n),n)}.$asyncbind(this,n),n)}.$asyncbind(this))}}]),e}();t.default=u},function(e,t,n){"use strict";function r(e,t){for(var n=""+t,r="return "+n,o=n.match(/.*\(([^)]*)\)/)[1],i=/['"]!!!([^'"]*)['"]/g,a=[];;){var u=i.exec(r);if(!u)break;a.push(u)}return a.reverse().forEach(function(t){r=r.slice(0,t.index)+e[t[1]]+r.substr(t.index+t[0].length)}),r=r.replace(/\/\*[^*]*\*\//g," ").replace(/\s+/g," "),Function(o,r)()}function o(e,t){if(Function.prototype.$asyncspawn||Object.defineProperty(Function.prototype,"$asyncspawn",{value:o,enumerable:!1,configurable:!0,writable:!0}),this instanceof Function){var n=this;return new e(function(e,r){function o(t,n){var a;try{if(a=t.call(i,n),a.done){if(a.value!==e){if(a.value&&a.value===a.value.then)return a.value(e,r);e&&e(a.value),e=null}return}a.value.then?a.value.then(function(e){o(i.next,e)},function(e){o(i.throw,e)}):o(i.next,a.value)}catch(e){return r&&r(e),void(r=null)}}var i=n.call(t,e,r);o(i.next)})}}var i=r({zousan:""+n(16),thenable:""+n(15)},function e(t,n){function r(){return o.apply(t,arguments)}Function.prototype.$asyncbind||Object.defineProperty(Function.prototype,"$asyncbind",{value:e,enumerable:!1,configurable:!0,writable:!0}),e.trampoline||(e.trampoline=function(e,t,n,r,o){return function i(a){for(;a;){if(a.then)return a=a.then(i,r),o?void 0:a;try{if(a.pop){if(a.length)return a.pop()?t.call(e):a;a=n}else a=a.call(e)}catch(e){return r(e)}}}}),e.LazyThenable||(e.LazyThenable="!!!thenable"(),e.EagerThenable=e.Thenable=(e.EagerThenableFactory="!!!zousan")());var o=this;switch(n){case!0:return new e.Thenable(r);case 0:return new e.LazyThenable(r);case void 0:return r.then=r,r;default:return function(){try{return o.apply(t,arguments)}catch(e){return n(e)}}}});i(),o(),e.exports={$asyncbind:i,$asyncspawn:o}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=t.createElement=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,r=document.createElement(e);return r.className=t,n&&n.appendChild(r),r};t.createScriptElement=function(e,t){var r=n("script",null,document.body);return r.setAttribute("type","text/javascript"),new Promise(function(n){window[t]=function(e){r.remove(),delete window[t],n(e)},r.setAttribute("src",e)})},t.addClassName=function(e,t){e&&!e.classList.contains(t)&&e.classList.add(t)},t.removeClassName=function(e,t){e&&e.classList.contains(t)&&e.classList.remove(t)}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=t.ENTER_KEY=13,r=t.ESCAPE_KEY=27,o=t.ARROW_DOWN_KEY=40,i=t.ARROW_UP_KEY=38,a=t.ARROW_LEFT_KEY=37,u=t.ARROW_RIGHT_KEY=39;t.SPECIAL_KEYS=[n,r,o,i,a,u]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=n(2),a=(r(i),function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}()),u=n(3),s=n(4),c=function(){function e(){var t=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.handleSubmit,i=void 0===r?function(){}:r,a=n.searchLabel,s=void 0===a?"search":a,c=n.classNames,l=void 0===c?{}:c;o(this,e);var f=(0,u.createElement)("div",["geosearch",l.container].join(" ")),p=(0,u.createElement)("form",["",l.form].join(" "),f),d=(0,u.createElement)("input",["glass",l.input].join(" "),p);d.type="text",d.placeholder=s,d.addEventListener("input",function(e){t.onInput(e)},!1),d.addEventListener("keyup",function(e){t.onKeyUp(e)},!1),d.addEventListener("keypress",function(e){t.onKeyPress(e)},!1),d.addEventListener("focus",function(e){t.onFocus(e)},!1),d.addEventListener("blur",function(e){t.onBlur(e)},!1),this.elements={container:f,form:p,input:d},this.handleSubmit=i}return a(e,[{key:"onFocus",value:function(){(0,u.addClassName)(this.elements.form,"active")}},{key:"onBlur",value:function(){(0,u.removeClassName)(this.elements.form,"active")}},{key:"onSubmit",value:function(e){return new Promise(function(t,n){var r,o,i;return e.preventDefault(),e.stopPropagation(),r=this.elements,o=r.input,i=r.container,(0,u.removeClassName)(i,"error"),(0,u.addClassName)(i,"pending"),this.handleSubmit({query:o.value}).then(function(e){return(0,u.removeClassName)(i,"pending"),t()}.$asyncbind(this,n),n)}.$asyncbind(this))}},{key:"onInput",value:function(){var e=this.elements.container;this.hasError&&((0,u.removeClassName)(e,"error"),this.hasError=!1)}},{key:"onKeyUp",value:function(e){var t=this.elements,n=t.container,r=t.input;e.keyCode===s.ESCAPE_KEY&&((0,u.removeClassName)(n,"pending"),(0,u.removeClassName)(n,"active"),r.value="",document.body.focus(),document.body.blur())}},{key:"onKeyPress",value:function(e){e.keyCode===s.ENTER_KEY&&this.onSubmit(e)}},{key:"setQuery",value:function(e){var t=this.elements.input;t.value=e}}]),e}();t.default=c},function(e,t){function n(){throw Error("setTimeout has not been defined")}function r(){throw Error("clearTimeout has not been defined")}function o(e){if(l===setTimeout)return setTimeout(e,0);if((l===n||!l)&&setTimeout)return l=setTimeout,setTimeout(e,0);try{return l(e,0)}catch(t){try{return l.call(null,e,0)}catch(t){return l.call(this,e,0)}}}function i(e){if(f===clearTimeout)return clearTimeout(e);if((f===r||!f)&&clearTimeout)return f=clearTimeout,clearTimeout(e);try{return f(e)}catch(t){try{return f.call(null,e)}catch(t){return f.call(this,e)}}}function a(){v&&d&&(v=!1,d.length?h=d.concat(h):m=-1,h.length&&u())}function u(){if(!v){var e=o(a);v=!0;for(var t=h.length;t;){for(d=h,h=[];++m1)for(var n=1;arguments.length>n;n++)t[n-1]=arguments[n];h.push(new s(e,t)),1!==h.length||v||o(u)},s.prototype.run=function(){this.fun.apply(null,this.array)},p.title="browser",p.browser=!0,p.env={},p.argv=[],p.version="",p.versions={},p.on=c,p.addListener=c,p.once=c,p.off=c,p.removeListener=c,p.removeAllListeners=c,p.emit=c,p.binding=function(e){throw Error("process.binding is not supported")},p.cwd=function(){return"/"},p.chdir=function(e){throw Error("process.chdir is not supported")},p.umask=function(){return 0}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var o=n(8);Object.defineProperty(t,"GeoSearchControl",{enumerable:!0,get:function(){return r(o).default}});var i=n(5);Object.defineProperty(t,"SearchElement",{enumerable:!0,get:function(){return r(i).default}});var a=n(9);Object.defineProperty(t,"BingProvider",{enumerable:!0,get:function(){return r(a).default}});var u=n(10);Object.defineProperty(t,"EsriProvider",{enumerable:!0,get:function(){return r(u).default}});var s=n(11);Object.defineProperty(t,"GoogleProvider",{enumerable:!0,get:function(){return r(s).default}});var c=n(12);Object.defineProperty(t,"OpenStreetMapProvider",{enumerable:!0,get:function(){return r(c).default}});var l=n(1);Object.defineProperty(t,"Provider",{enumerable:!0,get:function(){return r(l).default}})},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(){if(!L||!L.Control||!L.Control.extend)throw Error("Leaflet must be loaded before instantiating the GeoSearch control");for(var e=L.Control.extend(b),t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];return new(Function.prototype.bind.apply(e,[null].concat(n)))}Object.defineProperty(t,"__esModule",{value:!0});var i=n(2),a=(r(i),Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e});t.default=o;var u=n(14),s=r(u),c=n(5),l=r(c),f=n(13),p=r(f),d=n(3),h=n(4),v=function(){return{position:"topleft",style:"button",showMarker:!0,showPopup:!1,popupFormat:function(e){var t=e.result;return""+t.label},marker:{icon:new L.Icon.Default,draggable:!1},maxMarkers:1,retainZoomLevel:!1,animateZoom:!0,searchLabel:"Enter address",notFoundMessage:"Sorry, that address could not be found.",messageHideDelay:3e3,zoomLevel:18,classNames:{container:"leaflet-bar leaflet-control leaflet-control-geosearch",button:"leaflet-bar-part leaflet-bar-part-single",resetButton:"reset",msgbox:"leaflet-bar message",form:"",input:""},autoComplete:!0,autoCompleteDelay:250,autoClose:!1,keepResult:!1}},m={},y=["dragging","touchZoom","doubleClickZoom","scrollWheelZoom","boxZoom","keyboard"],b={initialize:function(e){var t=this;this.markers=new L.FeatureGroup,this.handlersDisabled=!1,this.options=a({},v(),e);var n=this.options,r=n.style,o=n.classNames,i=n.searchLabel,u=n.autoComplete,c=n.autoCompleteDelay;"button"!==r&&(this.options.classNames.container+=" "+e.style),this.searchElement=new l.default(a({},this.options,{handleSubmit:function(e){return t.onSubmit(e)}}));var f=this.searchElement.elements,h=f.container,m=f.form,y=f.input,b=(0,d.createElement)("a",o.button,h);b.title=i,b.href="#",b.addEventListener("click",function(e){t.onClick(e)},!1);var g=(0,d.createElement)("a",o.resetButton,m);g.innerHTML="X",b.href="#",g.addEventListener("click",function(){t.clearResults(null,!0)},!1),u&&(this.resultList=new p.default({handleClick:function(e){var n=e.result;y.value=n.label,t.onSubmit({query:n.label})}}),m.appendChild(this.resultList.elements.container),y.addEventListener("keyup",(0,s.default)(function(e){return t.autoSearch(e)},c),!0),y.addEventListener("keydown",function(e){return t.selectResult(e)},!0),y.addEventListener("keydown",function(e){return t.clearResults(e,!0)},!0)),m.addEventListener("mouseenter",function(e){return t.disableHandlers(e)},!0),m.addEventListener("mouseleave",function(e){return t.restoreHandlers(e)},!0),this.elements={button:b,resetButton:g}},onAdd:function(e){var t=this.options,n=t.showMarker,r=t.style;if(this.map=e,n&&this.markers.addTo(e),"bar"===r){var o=this.searchElement.elements.form,i=e.getContainer().querySelector(".leaflet-control-container"),a=(0,d.createElement)("div","leaflet-control-geosearch bar");a.appendChild(o),i.appendChild(a),this.elements.container=a}return this.searchElement.elements.container},onRemove:function(){var e=this.elements.container;return e&&e.remove(),this},onClick:function(e){e.preventDefault();var t=this.searchElement.elements,n=t.container,r=t.input;n.classList.contains("active")?((0,d.removeClassName)(n,"active"),this.clearResults()):((0,d.addClassName)(n,"active"),r.focus())},disableHandlers:function(e){var t=this,n=this.searchElement.elements.form;this.handlersDisabled||e&&e.target!==n||(this.handlersDisabled=!0,y.forEach(function(e){t.map[e]&&(m[e]=t.map[e].enabled(),t.map[e].disable())}))},restoreHandlers:function(e){var t=this,n=this.searchElement.elements.form;!this.handlersDisabled||e&&e.target!==n||(this.handlersDisabled=!1,y.forEach(function(e){m[e]&&t.map[e].enable()}))},selectResult:function(e){if([h.ENTER_KEY,h.ARROW_DOWN_KEY,h.ARROW_UP_KEY].includes(e.keyCode)){e.preventDefault();var t=this.searchElement.elements.input;if(e.keyCode===h.ENTER_KEY)return void this.onSubmit({query:t.value});var n=this.resultList,r=n.count()-1;if(r>=0){var o="ArrowDown"===e.code?~~n.selected+1:~~n.selected-1,i=0>o?r:o>r?0:o,a=n.select(i);t.value=a.label}}},clearResults:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(!e||e.keyCode===h.ESCAPE_KEY){var n=this.searchElement.elements.input,r=this.options,o=r.keepResult,i=r.autoComplete;!t&&o||(n.value="",this.markers.clearLayers()),i&&this.resultList.clear()}},autoSearch:function(e){return new Promise(function(t,n){function r(){return t()}var o,i,a;return h.SPECIAL_KEYS.includes(e.keyCode)?t():(o=e.target.value,i=this.options.provider,o.length?i.search({query:o}).then(function(e){return a=e,this.resultList.render(a),r.call(this)}.$asyncbind(this,n),n):(this.resultList.clear(),r.call(this)))}.$asyncbind(this))},onSubmit:function(e){return new Promise(function(t,n){var r,o;return r=this.options.provider,r.search(e).then(function(n){return o=n,o&&o.length>0&&this.showResult(o[0],e),t()}.$asyncbind(this,n),n)}.$asyncbind(this))},showResult:function(e,t){var n=t.query,r=this.options.autoClose,o=Object.keys(this.markers._layers);this.options.maxMarkers>o.length||this.markers.removeLayer(o[0]);var i=this.addMarker(e,n);this.centerMap(e),this.map.fireEvent("geosearch/showlocation",{location:e,marker:i}),r&&this.closeResults()},closeResults:function(){var e=this.searchElement.elements.container;e.classList.contains("active")&&(0,d.removeClassName)(e,"active"),this.restoreHandlers(),this.clearResults()},addMarker:function(e,t){var n=this,r=this.options,o=r.marker,i=r.showPopup,a=r.popupFormat,u=new L.Marker([e.y,e.x],o),s=e.label;return"function"==typeof a&&(s=a({query:t,result:e})),u.bindPopup(s),this.markers.addLayer(u),i&&u.openPopup(),o.draggable&&u.on("dragend",function(e){n.map.fireEvent("geosearch/marker/dragend",{location:u.getLatLng(),event:e})}),u},centerMap:function(e){var t=this.options,n=t.retainZoomLevel,r=t.animateZoom,o=new L.LatLngBounds(e.bounds),i=o.isValid()?o:this.markers.getBounds();!n&&o.isValid()?this.map.fitBounds(i,{animate:r}):this.map.setView(i.getCenter(),this.getZoom(),{animate:r})},getZoom:function(){var e=this.options,t=e.retainZoomLevel,n=e.zoomLevel;return t?this.map.getZoom():n}}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=n(2),s=(r(u),Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e}),c=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),l=n(1),f=r(l),p=n(3),d=function(e){function t(){return o(this,t),i(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),c(t,[{key:"endpoint",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.query,n=e.protocol,r=e.jsonp,o=this.options.params,i=this.getParamString(s({},o,{query:t,jsonp:r}));return n+"//dev.virtualearth.net/REST/v1/Locations?"+i}},{key:"parse",value:function(e){var t=e.data;return 0===t.resourceSets.length?[]:t.resourceSets[0].resources.map(function(e){return{x:e.point.coordinates[1],y:e.point.coordinates[0],label:e.address.formattedAddress,bounds:[[e.bbox[0],e.bbox[1]],[e.bbox[2],e.bbox[3]]],raw:e}})}},{key:"search",value:function(e){return new Promise(function(t,n){var r,o,i,a,u;return r=e.query,o=~location.protocol.indexOf("http")?location.protocol:"https:",i="BING_JSONP_CB_"+Date.now(),a=this.endpoint({query:r,protocol:o,jsonp:i}),(0,p.createScriptElement)(a,i).then(function(e){return u=e,t(this.parse({data:u}))}.$asyncbind(this,n),n)}.$asyncbind(this))}}]),t}(f.default);t.default=d},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),c=n(1),l=r(c),f=function(e){function t(){return o(this,t),i(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"endpoint",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.query,n=e.protocol,r=this.options.params,o=this.getParamString(u({},r,{f:"json",text:t}));return n+"//geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find?"+o}},{key:"parse",value:function(e){var t=e.data;return t.locations.map(function(e){return{x:e.feature.geometry.x,y:e.feature.geometry.y,label:e.name,bounds:[[e.extent.ymin,e.extent.xmin],[e.extent.ymax,e.extent.xmax]],raw:e}})}}]),t}(l.default);t.default=f},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),c=n(1),l=r(c),f=function(e){function t(){return o(this,t),i(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"endpoint",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.query,n=e.protocol,r=this.options.params,o=this.getParamString(u({},r,{address:t})),i=r&&r.key?"https:":n;return i+"//maps.googleapis.com/maps/api/geocode/json?"+o}},{key:"parse",value:function(e){var t=e.data;return t.results.map(function(e){return{x:e.geometry.location.lng,y:e.geometry.location.lat,label:e.formatted_address,bounds:[[e.geometry.viewport.southwest.lat,e.geometry.viewport.southwest.lng],[e.geometry.viewport.northeast.lat,e.geometry.viewport.northeast.lng]],raw:e}})}}]),t}(l.default);t.default=f},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var u=Object.assign||function(e){for(var t=1;arguments.length>t;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},s=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),c=n(1),l=r(c),f=function(e){function t(){return o(this,t),i(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return a(t,e),s(t,[{key:"endpoint",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.query,n=e.protocol,r=this.options.params,o=this.getParamString(u({},r,{format:"json",q:t}));return n+"//nominatim.openstreetmap.org/search?"+o}},{key:"parse",value:function(e){var t=e.data;return t.map(function(e){return{x:e.lon,y:e.lat,label:e.display_name,bounds:[[parseFloat(e.boundingbox[0]),parseFloat(e.boundingbox[2])],[parseFloat(e.boundingbox[1]),parseFloat(e.boundingbox[3])]],raw:e}})}}]),t}(l.default);t.default=f},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;t.length>n;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),i=n(3),a=function(){for(var e=arguments.length,t=Array(e),n=0;e>n;n++)t[n]=arguments[n];return t.join(" ").trim()},u=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.handleClick,o=void 0===n?function(){}:n,u=t.classNames,c=void 0===u?{}:u;r(this,e),s.call(this),this.props={handleClick:o,classNames:c},this.selected=-1;var l=(0,i.createElement)("div",a("results",c.container)),f=(0,i.createElement)("div",a(c.item));l.addEventListener("click",this.onClick,!0),this.elements={container:l,resultItem:f}}return o(e,[{key:"render",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=this.elements,n=t.container,r=t.resultItem;this.clear(),e.forEach(function(e,t){var o=r.cloneNode(!0);o.setAttribute("data-key",t),o.innerHTML=e.label,n.appendChild(o)}),e.length>0&&(0,i.addClassName)(n,"active"),this.results=e}},{key:"select",value:function(e){var t=this.elements.container;return Array.from(t.children).forEach(function(t,n){return n===e?(0,i.addClassName)(t,"active"):(0,i.removeClassName)(t,"active")}),this.selected=e,this.results[e]}},{key:"count",value:function(){return this.results?this.results.length:0}},{key:"clear",value:function(){var e=this.elements.container;for(this.selected=-1;e.lastChild;)e.removeChild(e.lastChild);(0,i.removeClassName)(e,"active")}}]),e}(),s=function(){var e=this;this.onClick=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.target,r=e.props.handleClick,o=e.elements.container;if(n.parentNode===o&&n.hasAttribute("data-key")){var i=n.getAttribute("data-key"),a=e.results[i];r({result:a})}}};t.default=u},function(e,t){(function(t){function n(e,t,n){function o(t){var n=v,r=m;return v=m=void 0,O=t,b=e.apply(r,n)}function i(e){return O=e,g=setTimeout(l,t),k?o(e):b}function s(e){var n=e-j,r=e-O,o=t-n;return P?_(o,y-r):o}function c(e){var n=e-j,r=e-O;return void 0===j||n>=t||0>n||P&&r>=y}function l(){var e=E();return c(e)?f(e):void(g=setTimeout(l,s(e)))}function f(e){return g=void 0,T&&v?o(e):(v=m=void 0,b)}function p(){void 0!==g&&clearTimeout(g),O=0,v=j=m=g=void 0}function d(){return void 0===g?b:f(E())}function h(){var e=E(),n=c(e);if(v=arguments,m=this,j=e,n){if(void 0===g)return i(j);if(P)return g=setTimeout(l,t),o(j)}return void 0===g&&(g=setTimeout(l,t)),b}var v,m,y,b,g,j,O=0,k=!1,P=!1,T=!0;if("function"!=typeof e)throw new TypeError(u);return t=a(t)||0,r(n)&&(k=!!n.leading,P="maxWait"in n,y=P?w(a(n.maxWait)||0,t):y,T="trailing"in n?!!n.trailing:T),h.cancel=p,h.flush=d,h}function r(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function o(e){return!!e&&"object"==typeof e}function i(e){return"symbol"==typeof e||o(e)&&g.call(e)==c}function a(e){if("number"==typeof e)return e;if(i(e))return s;if(r(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=r(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(l,"");var n=p.test(e);return n||d.test(e)?h(e.slice(2),n?2:8):f.test(e)?s:+e}var u="Expected a function",s=NaN,c="[object Symbol]",l=/^\s+|\s+$/g,f=/^[-+]0x[0-9a-f]+$/i,p=/^0b[01]+$/i,d=/^0o[0-7]+$/i,h=parseInt,v="object"==typeof t&&t&&t.Object===Object&&t,m="object"==typeof self&&self&&self.Object===Object&&self,y=v||m||Function("return this")(),b=Object.prototype,g=b.toString,w=Math.max,_=Math.min,E=function(){return y.Date.now()};e.exports=n}).call(t,function(){return this}())},function(e,t){e.exports=function(){function e(e){return e&&e instanceof Object&&"function"==typeof e.then}function t(n,r,o){try{var i=o?o(r):r;if(n===i)return n.reject(new TypeError("Promise resolution loop"));e(i)?i.then(function(e){t(n,e)},function(e){n.reject(e)}):n.resolve(i)}catch(e){n.reject(e)}}function n(){}function r(e){}function o(e,t){this.resolve=e,this.reject=t}function i(r,o){var i=new n;try{this._resolver(function(n){return e(n)?n.then(r,o):t(i,n,r)},function(e){t(i,e,o)})}catch(e){t(i,e,o)}return i}function a(e){this._resolver=e,this.then=i}return n.prototype={resolve:r,reject:r,then:o},a.resolve=function(e){return a.isThenable(e)?e:{then:function(t){return t(e)}}},a.isThenable=e,a}},function(e,t,n){(function(t,n){"use strict";e.exports=function(e){function r(e){if(e){var t=this;e(function(e){t.resolve(e)},function(e){t.reject(e)})}}function o(e,t){if("function"==typeof e.y)try{var n=e.y.call(void 0,t);e.p.resolve(n)}catch(t){e.p.reject(t)}else e.p.resolve(t)}function i(e,t){if("function"==typeof e.n)try{var n=e.n.call(void 0,t);e.p.resolve(n)}catch(t){e.p.reject(t)}else e.p.reject(t)}e=e||"object"==typeof t&&t.nextTick||"function"==typeof n&&n||function(e){setTimeout(e,0)};var a=function(){function t(){for(;n.length-r;){try{n[r]()}catch(e){}n[r++]=void 0,r===o&&(n.splice(0,o),r=0)}}var n=[],r=0,o=1024;return function(o){n.push(o),n.length-r===1&&e(t)}}();return r.prototype={resolve:function(e){if(void 0===this.state){if(e===this)return this.reject(new TypeError("Attempt to resolve promise with self"));var t=this;if(e&&("function"==typeof e||"object"==typeof e))try{var n=0,r=e.then;if("function"==typeof r)return void r.call(e,function(e){n++||t.resolve(e)},function(e){n++||t.reject(e)})}catch(e){return void(n||this.reject(e))}this.state=o,this.v=e,t.c&&a(function(){for(var n=0,r=t.c.length;r>n;n++)o(t.c[n],e)})}},reject:function(e){if(void 0===this.state){this.state=i,this.v=e;var t=this.c;t&&a(function(){for(var n=0,r=t.length;r>n;n++)i(t[n],e)})}},then:function(e,t){var n=new r,o={y:e,n:t,p:n};if(void 0===this.state)this.c?this.c.push(o):this.c=[o];else{var i=this.state,u=this.v;a(function(){i(o,u)})}return n}},r.resolve=function(e){if(e&&e instanceof r)return e;var t=new r;return t.resolve(e),t},r.reject=function(e){if(e&&e instanceof r)return e;var t=new r;return t.reject(e),t},r.version="2.3.3-nodent",r}}).call(t,n(6),n(18).setImmediate)},function(e,t,n){(function(e,t){!function(e,n){"use strict";function r(e){"function"!=typeof e&&(e=Function(""+e));for(var t=Array(arguments.length-1),n=0;t.length>n;n++)t[n]=arguments[n+1];var r={callback:e,args:t};return v[h]=r,d(h),h++}function o(e){delete v[e]}function i(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}function a(e){if(m)setTimeout(a,0,e);else{var t=v[e];if(t){m=!0;try{i(t)}finally{o(e),m=!1}}}}function u(){d=function(e){t.nextTick(function(){a(e)})}}function s(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}function c(){var t="setImmediate$"+Math.random()+"$",n=function(n){n.source===e&&"string"==typeof n.data&&0===n.data.indexOf(t)&&a(+n.data.slice(t.length))};e.addEventListener?e.addEventListener("message",n,!1):e.attachEvent("onmessage",n),d=function(n){e.postMessage(t+n,"*")}}function l(){var e=new MessageChannel;e.port1.onmessage=function(e){var t=e.data;a(t)},d=function(t){e.port2.postMessage(t)}}function f(){var e=y.documentElement;d=function(t){var n=y.createElement("script");n.onreadystatechange=function(){a(t),n.onreadystatechange=null,e.removeChild(n),n=null},e.appendChild(n)}}function p(){d=function(e){setTimeout(a,0,e)}}if(!e.setImmediate){var d,h=1,v={},m=!1,y=e.document,b=Object.getPrototypeOf&&Object.getPrototypeOf(e);b=b&&b.setTimeout?b:e,"[object process]"==={}.toString.call(e.process)?u():s()?c():e.MessageChannel?l():y&&"onreadystatechange"in y.createElement("script")?f():p(),b.setImmediate=r,b.clearImmediate=o}}("undefined"==typeof self?void 0===e?this:e:self)}).call(t,function(){return this}(),n(6))},function(e,t,n){function r(e,t){this._id=e,this._clearFn=t}var o=Function.prototype.apply;t.setTimeout=function(){return new r(o.call(setTimeout,window,arguments),clearTimeout)},t.setInterval=function(){return new r(o.call(setInterval,window,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},r.prototype.unref=r.prototype.ref=function(){},r.prototype.close=function(){this._clearFn.call(window,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;0>t||(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(17),t.setImmediate=setImmediate,t.clearImmediate=clearImmediate}])}); -------------------------------------------------------------------------------- /static/js/leaflet-control.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Name Data passed Description 3 | 4 | Managed Events: 5 | search:locationfound {latlng, title, layer} fired after moved and show markerLocation 6 | search:expanded {} fired after control was expanded 7 | search:collapsed {} fired after control was collapsed 8 | 9 | Public methods: 10 | setLayer() L.LayerGroup() set layer search at runtime 11 | showAlert() 'Text message' show alert message 12 | searchText() 'Text searched' search text by external code 13 | */ 14 | 15 | //TODO implement can do research on multiple sources layers and remote 16 | //TODO history: false, //show latest searches in tooltip 17 | //FIXME option condition problem {autoCollapse: true, markerLocation: true} not show location 18 | //FIXME option condition problem {autoCollapse: false } 19 | // 20 | //TODO here insert function search inputText FIRST in _recordsCache keys and if not find results.. 21 | // run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip 22 | // 23 | //TODO change structure of _recordsCache 24 | // like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...} 25 | // in this mode every record can have a free structure of attributes, only 'loc' is required 26 | //TODO important optimization!!! always append data in this._recordsCache 27 | // now _recordsCache content is emptied and replaced with new data founded 28 | // always appending data on _recordsCache give the possibility of caching ajax, jsonp and layersearch! 29 | // 30 | //TODO here insert function search inputText FIRST in _recordsCache keys and if not find results.. 31 | // run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip 32 | // 33 | //TODO change structure of _recordsCache 34 | // like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...} 35 | // in this way every record can have a free structure of attributes, only 'loc' is required 36 | 37 | (function (factory) { 38 | if(typeof define === 'function' && define.amd) { 39 | //AMD 40 | define(['leaflet'], factory); 41 | } else if(typeof module !== 'undefined') { 42 | // Node/CommonJS 43 | module.exports = factory(require('leaflet')); 44 | } else { 45 | // Browser globals 46 | if(typeof window.L === 'undefined') 47 | throw 'Leaflet must be loaded first'; 48 | factory(window.L); 49 | } 50 | })(function (L) { 51 | 52 | 53 | L.Control.Search = L.Control.extend({ 54 | 55 | includes: L.version[0]==='1' ? L.Evented.prototype : L.Mixin.Events, 56 | 57 | options: { 58 | url: '', //url for search by ajax request, ex: "search.php?q={s}". Can be function to returns string for dynamic parameter setting 59 | layer: null, //layer where search markers(is a L.LayerGroup) 60 | sourceData: null, //function to fill _recordsCache, passed searching text by first param and callback in second 61 | //TODO implements uniq option 'sourceData' to recognizes source type: url,array,callback or layer 62 | jsonpParam: null, //jsonp param name for search by jsonp service, ex: "callback" 63 | propertyLoc: 'loc', //field for remapping location, using array: ['latname','lonname'] for select double fields(ex. ['lat','lon'] ) support dotted format: 'prop.subprop.title' 64 | propertyName: 'title', //property in marker.options(or feature.properties for vector layer) trough filter elements in layer, 65 | formatData: null, //callback for reformat all data from source to indexed data object 66 | filterData: null, //callback for filtering data from text searched, params: textSearch, allRecords 67 | moveToLocation: null, //callback run on location found, params: latlng, title, map 68 | buildTip: null, //function to return row tip html node(or html string), receive text tooltip in first param 69 | container: '', //container id to insert Search Control 70 | zoom: null, //default zoom level for move to location 71 | minLength: 1, //minimal text length for autocomplete 72 | initial: true, //search elements only by initial text 73 | casesensitive: false, //search elements in case sensitive text 74 | autoType: true, //complete input with first suggested result and select this filled-in text. 75 | delayType: 400, //delay while typing for show tooltip 76 | tooltipLimit: -1, //limit max results to show in tooltip. -1 for no limit, 0 for no results 77 | tipAutoSubmit: true, //auto map panTo when click on tooltip 78 | firstTipSubmit: false, //auto select first result con enter click 79 | autoResize: true, //autoresize on input change 80 | collapsed: true, //collapse search control at startup 81 | autoCollapse: false, //collapse search control after submit(on button or on tips if enabled tipAutoSubmit) 82 | autoCollapseTime: 1200, //delay for autoclosing alert and collapse after blur 83 | textErr: 'Location not found', //error message 84 | textCancel: 'Cancel', //title in cancel button 85 | textPlaceholder: 'Search...', //placeholder value 86 | hideMarkerOnCollapse: false, //remove circle and marker on search control collapsed 87 | position: 'topleft', 88 | marker: { //custom L.Marker or false for hide 89 | icon: false, //custom L.Icon for maker location or false for hide 90 | animate: true, //animate a circle over location found 91 | circle: { //draw a circle in location found 92 | radius: 10, 93 | weight: 3, 94 | color: '#e03', 95 | stroke: true, 96 | fill: false 97 | } 98 | } 99 | }, 100 | 101 | _getPath: function(obj, prop) { 102 | var parts = prop.split('.'), 103 | last = parts.pop(), 104 | len = parts.length, 105 | cur = parts[0], 106 | i = 1; 107 | 108 | if(len > 0) 109 | while((obj = obj[cur]) && i < len) 110 | cur = parts[i++]; 111 | 112 | if(obj) 113 | return obj[last]; 114 | }, 115 | 116 | _isObject: function(obj) { 117 | return Object.prototype.toString.call(obj) === "[object Object]"; 118 | }, 119 | 120 | initialize: function(options) { 121 | L.Util.setOptions(this, options || {}); 122 | this._inputMinSize = this.options.textPlaceholder ? this.options.textPlaceholder.length : 10; 123 | this._layer = this.options.layer || new L.LayerGroup(); 124 | this._filterData = this.options.filterData || this._defaultFilterData; 125 | this._formatData = this.options.formatData || this._defaultFormatData; 126 | this._moveToLocation = this.options.moveToLocation || this._defaultMoveToLocation; 127 | this._autoTypeTmp = this.options.autoType; //useful for disable autoType temporarily in delete/backspace keydown 128 | this._countertips = 0; //number of tips items 129 | this._recordsCache = {}; //key,value table! to store locations! format: key,latlng 130 | this._curReq = null; 131 | }, 132 | 133 | onAdd: function (map) { 134 | this._map = map; 135 | this._container = L.DomUtil.create('div', 'leaflet-control-search'); 136 | this._input = this._createInput(this.options.textPlaceholder, 'search-input'); 137 | this._tooltip = this._createTooltip('search-tooltip'); 138 | this._cancel = this._createCancel(this.options.textCancel, 'search-cancel'); 139 | this._button = this._createButton(this.options.textPlaceholder, 'search-button'); 140 | this._alert = this._createAlert('search-alert'); 141 | 142 | if(this.options.collapsed===false) 143 | this.expand(this.options.collapsed); 144 | 145 | if(this.options.marker) { 146 | 147 | if(this.options.marker instanceof L.Marker || this.options.marker instanceof L.CircleMarker) 148 | this._markerSearch = this.options.marker; 149 | 150 | else if(this._isObject(this.options.marker)) 151 | this._markerSearch = new L.Control.Search.Marker([0,0], this.options.marker); 152 | 153 | this._markerSearch._isMarkerSearch = true; 154 | } 155 | 156 | this.setLayer( this._layer ); 157 | 158 | map.on({ 159 | // 'layeradd': this._onLayerAddRemove, 160 | // 'layerremove': this._onLayerAddRemove 161 | 'resize': this._handleAutoresize 162 | }, this); 163 | return this._container; 164 | }, 165 | addTo: function (map) { 166 | 167 | if(this.options.container) { 168 | this._container = this.onAdd(map); 169 | this._wrapper = L.DomUtil.get(this.options.container); 170 | this._wrapper.style.position = 'relative'; 171 | this._wrapper.appendChild(this._container); 172 | } 173 | else 174 | L.Control.prototype.addTo.call(this, map); 175 | 176 | return this; 177 | }, 178 | 179 | onRemove: function(map) { 180 | this._recordsCache = {}; 181 | // map.off({ 182 | // 'layeradd': this._onLayerAddRemove, 183 | // 'layerremove': this._onLayerAddRemove 184 | // }, this); 185 | }, 186 | 187 | // _onLayerAddRemove: function(e) { 188 | // //without this, run setLayer also for each Markers!! to optimize! 189 | // if(e.layer instanceof L.LayerGroup) 190 | // if( L.stamp(e.layer) != L.stamp(this._layer) ) 191 | // this.setLayer(e.layer); 192 | // }, 193 | 194 | setLayer: function(layer) { //set search layer at runtime 195 | //this.options.layer = layer; //setting this, run only this._recordsFromLayer() 196 | this._layer = layer; 197 | this._layer.addTo(this._map); 198 | return this; 199 | }, 200 | 201 | showAlert: function(text) { 202 | var self = this; 203 | text = text || this.options.textErr; 204 | this._alert.style.display = 'block'; 205 | this._alert.innerHTML = text; 206 | clearTimeout(this.timerAlert); 207 | 208 | this.timerAlert = setTimeout(function() { 209 | self.hideAlert(); 210 | },this.options.autoCollapseTime); 211 | return this; 212 | }, 213 | 214 | hideAlert: function() { 215 | this._alert.style.display = 'none'; 216 | return this; 217 | }, 218 | 219 | cancel: function() { 220 | this._input.value = ''; 221 | this._handleKeypress({ keyCode: 8 });//simulate backspace keypress 222 | this._input.size = this._inputMinSize; 223 | this._input.focus(); 224 | this._cancel.style.display = 'none'; 225 | this._hideTooltip(); 226 | return this; 227 | }, 228 | 229 | expand: function(toggle) { 230 | toggle = typeof toggle === 'boolean' ? toggle : true; 231 | this._input.style.display = 'block'; 232 | L.DomUtil.addClass(this._container, 'search-exp'); 233 | if ( toggle !== false ) { 234 | this._input.focus(); 235 | this._map.on('dragstart click', this.collapse, this); 236 | } 237 | this.fire('search:expanded'); 238 | return this; 239 | }, 240 | 241 | collapse: function() { 242 | this._hideTooltip(); 243 | this.cancel(); 244 | this._alert.style.display = 'none'; 245 | this._input.blur(); 246 | if(this.options.collapsed) 247 | { 248 | this._input.style.display = 'none'; 249 | this._cancel.style.display = 'none'; 250 | L.DomUtil.removeClass(this._container, 'search-exp'); 251 | if (this.options.hideMarkerOnCollapse) { 252 | this._map.removeLayer(this._markerSearch); 253 | } 254 | this._map.off('dragstart click', this.collapse, this); 255 | } 256 | this.fire('search:collapsed'); 257 | return this; 258 | }, 259 | 260 | collapseDelayed: function() { //collapse after delay, used on_input blur 261 | var self = this; 262 | if (!this.options.autoCollapse) return this; 263 | clearTimeout(this.timerCollapse); 264 | this.timerCollapse = setTimeout(function() { 265 | self.collapse(); 266 | }, this.options.autoCollapseTime); 267 | return this; 268 | }, 269 | 270 | collapseDelayedStop: function() { 271 | clearTimeout(this.timerCollapse); 272 | return this; 273 | }, 274 | 275 | ////start DOM creations 276 | _createAlert: function(className) { 277 | var alert = L.DomUtil.create('div', className, this._container); 278 | alert.style.display = 'none'; 279 | 280 | L.DomEvent 281 | .on(alert, 'click', L.DomEvent.stop, this) 282 | .on(alert, 'click', this.hideAlert, this); 283 | 284 | return alert; 285 | }, 286 | 287 | _createInput: function (text, className) { 288 | var label = L.DomUtil.create('label', className, this._container); 289 | var input = L.DomUtil.create('input', className, this._container); 290 | input.type = 'text'; 291 | input.size = this._inputMinSize; 292 | input.value = ''; 293 | input.autocomplete = 'off'; 294 | input.autocorrect = 'off'; 295 | input.autocapitalize = 'off'; 296 | input.placeholder = text; 297 | input.style.display = 'none'; 298 | input.role = 'search'; 299 | input.id = input.role + input.type + input.size; 300 | 301 | label.htmlFor = input.id; 302 | label.style.display = 'none'; 303 | label.value = text; 304 | 305 | L.DomEvent 306 | .disableClickPropagation(input) 307 | .on(input, 'keyup', this._handleKeypress, this) 308 | .on(input, 'blur', this.collapseDelayed, this) 309 | .on(input, 'focus', this.collapseDelayedStop, this); 310 | 311 | return input; 312 | }, 313 | 314 | _createCancel: function (title, className) { 315 | var cancel = L.DomUtil.create('a', className, this._container); 316 | cancel.href = '#'; 317 | cancel.title = title; 318 | cancel.style.display = 'none'; 319 | cancel.innerHTML = "";//imageless(see css) 320 | 321 | L.DomEvent 322 | .on(cancel, 'click', L.DomEvent.stop, this) 323 | .on(cancel, 'click', this.cancel, this); 324 | 325 | return cancel; 326 | }, 327 | 328 | _createButton: function (title, className) { 329 | var button = L.DomUtil.create('a', className, this._container); 330 | button.href = '#'; 331 | button.title = title; 332 | 333 | L.DomEvent 334 | .on(button, 'click', L.DomEvent.stop, this) 335 | .on(button, 'click', this._handleSubmit, this) 336 | .on(button, 'focus', this.collapseDelayedStop, this) 337 | .on(button, 'blur', this.collapseDelayed, this); 338 | 339 | return button; 340 | }, 341 | 342 | _createTooltip: function(className) { 343 | var self = this; 344 | var tool = L.DomUtil.create('ul', className, this._container); 345 | tool.style.display = 'none'; 346 | L.DomEvent 347 | .disableClickPropagation(tool) 348 | .on(tool, 'blur', this.collapseDelayed, this) 349 | .on(tool, 'mousewheel', function(e) { 350 | self.collapseDelayedStop(); 351 | L.DomEvent.stopPropagation(e);//disable zoom map 352 | }, this) 353 | .on(tool, 'mouseover', function(e) { 354 | self.collapseDelayedStop(); 355 | }, this); 356 | return tool; 357 | }, 358 | 359 | _createTip: function(text, val) {//val is object in recordCache, usually is Latlng 360 | var tip; 361 | 362 | if(this.options.buildTip) 363 | { 364 | tip = this.options.buildTip.call(this, text, val); //custom tip node or html string 365 | if(typeof tip === 'string') 366 | { 367 | var tmpNode = L.DomUtil.create('div'); 368 | tmpNode.innerHTML = tip; 369 | tip = tmpNode.firstChild; 370 | } 371 | } 372 | else 373 | { 374 | tip = L.DomUtil.create('li', ''); 375 | tip.innerHTML = text; 376 | } 377 | 378 | L.DomUtil.addClass(tip, 'search-tip'); 379 | tip._text = text; //value replaced in this._input and used by _autoType 380 | 381 | if(this.options.tipAutoSubmit) 382 | L.DomEvent 383 | .disableClickPropagation(tip) 384 | .on(tip, 'click', L.DomEvent.stop, this) 385 | .on(tip, 'click', function(e) { 386 | this._input.value = text; 387 | this._handleAutoresize(); 388 | this._input.focus(); 389 | this._hideTooltip(); 390 | this._handleSubmit(); 391 | }, this); 392 | 393 | return tip; 394 | }, 395 | 396 | //////end DOM creations 397 | 398 | _getUrl: function(text) { 399 | return (typeof this.options.url === 'function') ? this.options.url(text) : this.options.url; 400 | }, 401 | 402 | _defaultFilterData: function(text, records) { 403 | 404 | var I, icase, regSearch, frecords = {}; 405 | 406 | text = text.replace(/[.*+?^${}()|[\]\\]/g, ''); //sanitize remove all special characters 407 | if(text==='') 408 | return []; 409 | 410 | I = this.options.initial ? '^' : ''; //search only initial text 411 | icase = !this.options.casesensitive ? 'i' : undefined; 412 | 413 | regSearch = new RegExp(I + text, icase); 414 | 415 | //TODO use .filter or .map 416 | for(var key in records) { 417 | if( regSearch.test(key) ) 418 | frecords[key]= records[key]; 419 | } 420 | 421 | return frecords; 422 | }, 423 | 424 | showTooltip: function(records) { 425 | 426 | 427 | this._countertips = 0; 428 | this._tooltip.innerHTML = ''; 429 | this._tooltip.currentSelection = -1; //inizialized for _handleArrowSelect() 430 | 431 | if(this.options.tooltipLimit) 432 | { 433 | for(var key in records)//fill tooltip 434 | { 435 | if(this._countertips === this.options.tooltipLimit) 436 | break; 437 | 438 | this._countertips++; 439 | 440 | this._tooltip.appendChild( this._createTip(key, records[key]) ); 441 | } 442 | } 443 | 444 | if(this._countertips > 0) 445 | { 446 | this._tooltip.style.display = 'block'; 447 | 448 | if(this._autoTypeTmp) 449 | this._autoType(); 450 | 451 | this._autoTypeTmp = this.options.autoType;//reset default value 452 | } 453 | else 454 | this._hideTooltip(); 455 | 456 | this._tooltip.scrollTop = 0; 457 | 458 | return this._countertips; 459 | }, 460 | 461 | _hideTooltip: function() { 462 | this._tooltip.style.display = 'none'; 463 | this._tooltip.innerHTML = ''; 464 | return 0; 465 | }, 466 | 467 | _defaultFormatData: function(json) { //default callback for format data to indexed data 468 | var self = this, 469 | propName = this.options.propertyName, 470 | propLoc = this.options.propertyLoc, 471 | i, jsonret = {}; 472 | 473 | if( L.Util.isArray(propLoc) ) 474 | for(i in json) 475 | jsonret[ self._getPath(json[i],propName) ]= L.latLng( json[i][ propLoc[0] ], json[i][ propLoc[1] ] ); 476 | else 477 | for(i in json) 478 | jsonret[ self._getPath(json[i],propName) ]= L.latLng( self._getPath(json[i],propLoc) ); 479 | //TODO throw new Error("propertyName '"+propName+"' not found in JSON data"); 480 | return jsonret; 481 | }, 482 | 483 | _recordsFromJsonp: function(text, callAfter) { //extract searched records from remote jsonp service 484 | L.Control.Search.callJsonp = callAfter; 485 | var script = L.DomUtil.create('script','leaflet-search-jsonp', document.getElementsByTagName('body')[0] ), 486 | url = L.Util.template(this._getUrl(text)+'&'+this.options.jsonpParam+'=L.Control.Search.callJsonp', {s: text}); //parsing url 487 | //rnd = '&_='+Math.floor(Math.random()*10000); 488 | //TODO add rnd param or randomize callback name! in recordsFromJsonp 489 | script.type = 'text/javascript'; 490 | script.src = url; 491 | return { abort: function() { script.parentNode.removeChild(script); } }; 492 | }, 493 | 494 | _recordsFromAjax: function(text, callAfter) { //Ajax request 495 | if (window.XMLHttpRequest === undefined) { 496 | window.XMLHttpRequest = function() { 497 | try { return new ActiveXObject("Microsoft.XMLHTTP.6.0"); } 498 | catch (e1) { 499 | try { return new ActiveXObject("Microsoft.XMLHTTP.3.0"); } 500 | catch (e2) { throw new Error("XMLHttpRequest is not supported"); } 501 | } 502 | }; 503 | } 504 | var IE8or9 = ( L.Browser.ie && !window.atob && document.querySelector ), 505 | request = IE8or9 ? new XDomainRequest() : new XMLHttpRequest(), 506 | url = L.Util.template(this._getUrl(text), {s: text}); 507 | 508 | //rnd = '&_='+Math.floor(Math.random()*10000); 509 | //TODO add rnd param or randomize callback name! in recordsFromAjax 510 | 511 | request.open("GET", url); 512 | 513 | 514 | request.onload = function() { 515 | callAfter( JSON.parse(request.responseText) ); 516 | }; 517 | request.onreadystatechange = function() { 518 | if(request.readyState === 4 && request.status === 200) { 519 | this.onload(); 520 | } 521 | }; 522 | 523 | request.send(); 524 | return request; 525 | }, 526 | 527 | _searchInLayer: function(layer, retRecords, propName) { 528 | var self = this, loc; 529 | 530 | if(layer instanceof L.Control.Search.Marker) return; 531 | 532 | if(layer instanceof L.Marker || layer instanceof L.CircleMarker) 533 | { 534 | if(self._getPath(layer.options,propName)) 535 | { 536 | loc = layer.getLatLng(); 537 | loc.layer = layer; 538 | retRecords[ self._getPath(layer.options,propName) ] = loc; 539 | } 540 | else if(self._getPath(layer.feature.properties,propName)) 541 | { 542 | loc = layer.getLatLng(); 543 | loc.layer = layer; 544 | retRecords[ self._getPath(layer.feature.properties,propName) ] = loc; 545 | } 546 | else { 547 | //throw new Error("propertyName '"+propName+"' not found in marker"); 548 | console.warn("propertyName '"+propName+"' not found in marker"); 549 | } 550 | } 551 | if(layer instanceof L.Path || layer instanceof L.Polyline || layer instanceof L.Polygon) 552 | { 553 | if(self._getPath(layer.options,propName)) 554 | { 555 | loc = layer.getBounds().getCenter(); 556 | loc.layer = layer; 557 | retRecords[ self._getPath(layer.options,propName) ] = loc; 558 | } 559 | else if(self._getPath(layer.feature.properties,propName)) 560 | { 561 | loc = layer.getBounds().getCenter(); 562 | loc.layer = layer; 563 | retRecords[ self._getPath(layer.feature.properties,propName) ] = loc; 564 | } 565 | else { 566 | //throw new Error("propertyName '"+propName+"' not found in shape"); 567 | console.warn("propertyName '"+propName+"' not found in shape"); 568 | } 569 | } 570 | else if(layer.hasOwnProperty('feature'))//GeoJSON 571 | { 572 | if(layer.feature.properties.hasOwnProperty(propName)) 573 | { 574 | if(layer.getLatLng && typeof layer.getLatLng === 'function') { 575 | loc = layer.getLatLng(); 576 | loc.layer = layer; 577 | retRecords[ layer.feature.properties[propName] ] = loc; 578 | } else if(layer.getBounds && typeof layer.getBounds === 'function') { 579 | loc = layer.getBounds().getCenter(); 580 | loc.layer = layer; 581 | retRecords[ layer.feature.properties[propName] ] = loc; 582 | } else { 583 | console.warn("Unknown type of Layer"); 584 | } 585 | } 586 | else { 587 | //throw new Error("propertyName '"+propName+"' not found in feature"); 588 | console.warn("propertyName '"+propName+"' not found in feature"); 589 | } 590 | } 591 | else if(layer instanceof L.LayerGroup) 592 | { 593 | layer.eachLayer(function (layer) { 594 | self._searchInLayer(layer, retRecords, propName); 595 | }); 596 | } 597 | }, 598 | 599 | _recordsFromLayer: function() { //return table: key,value from layer 600 | var self = this, 601 | retRecords = {}, 602 | propName = this.options.propertyName; 603 | 604 | this._layer.eachLayer(function (layer) { 605 | self._searchInLayer(layer, retRecords, propName); 606 | }); 607 | 608 | return retRecords; 609 | }, 610 | 611 | _autoType: function() { 612 | 613 | //TODO implements autype without selection(useful for mobile device) 614 | 615 | var start = this._input.value.length, 616 | firstRecord = this._tooltip.firstChild ? this._tooltip.firstChild._text : '', 617 | end = firstRecord.length; 618 | 619 | if (firstRecord.indexOf(this._input.value) === 0) { // If prefix match 620 | this._input.value = firstRecord; 621 | this._handleAutoresize(); 622 | 623 | if (this._input.createTextRange) { 624 | var selRange = this._input.createTextRange(); 625 | selRange.collapse(true); 626 | selRange.moveStart('character', start); 627 | selRange.moveEnd('character', end); 628 | selRange.select(); 629 | } 630 | else if(this._input.setSelectionRange) { 631 | this._input.setSelectionRange(start, end); 632 | } 633 | else if(this._input.selectionStart) { 634 | this._input.selectionStart = start; 635 | this._input.selectionEnd = end; 636 | } 637 | } 638 | }, 639 | 640 | _hideAutoType: function() { // deselect text: 641 | 642 | var sel; 643 | if ((sel = this._input.selection) && sel.empty) { 644 | sel.empty(); 645 | } 646 | else if (this._input.createTextRange) { 647 | sel = this._input.createTextRange(); 648 | sel.collapse(true); 649 | var end = this._input.value.length; 650 | sel.moveStart('character', end); 651 | sel.moveEnd('character', end); 652 | sel.select(); 653 | } 654 | else { 655 | if (this._input.getSelection) { 656 | this._input.getSelection().removeAllRanges(); 657 | } 658 | this._input.selectionStart = this._input.selectionEnd; 659 | } 660 | }, 661 | 662 | _handleKeypress: function (e) { //run _input keyup event 663 | var self = this; 664 | 665 | switch(e.keyCode) 666 | { 667 | case 27://Esc 668 | this.collapse(); 669 | break; 670 | case 13://Enter 671 | if(this._countertips == 1 || (this.options.firstTipSubmit && this._countertips > 0)) 672 | if(this._tooltip.currentSelection == -1) 673 | this._handleArrowSelect(1); 674 | this._handleSubmit(); //do search 675 | break; 676 | case 38://Up 677 | this._handleArrowSelect(-1); 678 | break; 679 | case 40://Down 680 | this._handleArrowSelect(1); 681 | break; 682 | case 8://Backspace 683 | case 45://Insert 684 | case 46://Delete 685 | this._autoTypeTmp = false;//disable temporarily autoType 686 | break; 687 | case 37://Left 688 | case 39://Right 689 | case 16://Shift 690 | case 17://Ctrl 691 | case 35://End 692 | case 36://Home 693 | break; 694 | default://All keys 695 | 696 | if(this._input.value.length) 697 | this._cancel.style.display = 'block'; 698 | else 699 | this._cancel.style.display = 'none'; 700 | 701 | if(this._input.value.length >= this.options.minLength) 702 | { 703 | clearTimeout(this.timerKeypress); //cancel last search request while type in 704 | this.timerKeypress = setTimeout(function() { //delay before request, for limit jsonp/ajax request 705 | 706 | self._fillRecordsCache(); 707 | 708 | }, this.options.delayType); 709 | } 710 | else 711 | this._hideTooltip(); 712 | } 713 | 714 | this._handleAutoresize(); 715 | }, 716 | 717 | searchText: function(text) { 718 | var code = text.charCodeAt(text.length); 719 | 720 | this._input.value = text; 721 | 722 | this._input.style.display = 'block'; 723 | L.DomUtil.addClass(this._container, 'search-exp'); 724 | 725 | this._autoTypeTmp = false; 726 | 727 | this._handleKeypress({keyCode: code}); 728 | }, 729 | 730 | _fillRecordsCache: function() { 731 | 732 | var self = this, 733 | inputText = this._input.value, records; 734 | 735 | if(this._curReq && this._curReq.abort) 736 | this._curReq.abort(); 737 | //abort previous requests 738 | 739 | L.DomUtil.addClass(this._container, 'search-load'); 740 | 741 | if(this.options.layer) 742 | { 743 | //TODO _recordsFromLayer must return array of objects, formatted from _formatData 744 | this._recordsCache = this._recordsFromLayer(); 745 | 746 | records = this._filterData( this._input.value, this._recordsCache ); 747 | 748 | this.showTooltip( records ); 749 | 750 | L.DomUtil.removeClass(this._container, 'search-load'); 751 | } 752 | else 753 | { 754 | if(this.options.sourceData) 755 | this._retrieveData = this.options.sourceData; 756 | 757 | else if(this.options.url) //jsonp or ajax 758 | this._retrieveData = this.options.jsonpParam ? this._recordsFromJsonp : this._recordsFromAjax; 759 | 760 | this._curReq = this._retrieveData.call(this, inputText, function(data) { 761 | 762 | self._recordsCache = self._formatData.call(self, data); 763 | 764 | //TODO refact! 765 | if(self.options.sourceData) 766 | records = self._filterData( self._input.value, self._recordsCache ); 767 | else 768 | records = self._recordsCache; 769 | 770 | self.showTooltip( records ); 771 | 772 | L.DomUtil.removeClass(self._container, 'search-load'); 773 | }); 774 | } 775 | }, 776 | 777 | _handleAutoresize: function() { //autoresize this._input 778 | //TODO refact _handleAutoresize now is not accurate 779 | if (this._input.style.maxWidth != this._map._container.offsetWidth) //If maxWidth isn't the same as when first set, reset to current Map width 780 | this._input.style.maxWidth = L.DomUtil.getStyle(this._map._container, 'width'); 781 | 782 | if(this.options.autoResize && (this._container.offsetWidth + 45 < this._map._container.offsetWidth)) 783 | this._input.size = this._input.value.length= (searchTips.length - 1))) {// If at end of list. 794 | L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select'); 795 | } 796 | else if ((velocity == -1 ) && (this._tooltip.currentSelection <= 0)) { // Going back up to the search box. 797 | this._tooltip.currentSelection = -1; 798 | } 799 | else if (this._tooltip.style.display != 'none') { 800 | this._tooltip.currentSelection += velocity; 801 | 802 | L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select'); 803 | 804 | this._input.value = searchTips[this._tooltip.currentSelection]._text; 805 | 806 | // scroll: 807 | var tipOffsetTop = searchTips[this._tooltip.currentSelection].offsetTop; 808 | 809 | if (tipOffsetTop + searchTips[this._tooltip.currentSelection].clientHeight >= this._tooltip.scrollTop + this._tooltip.clientHeight) { 810 | this._tooltip.scrollTop = tipOffsetTop - this._tooltip.clientHeight + searchTips[this._tooltip.currentSelection].clientHeight; 811 | } 812 | else if (tipOffsetTop <= this._tooltip.scrollTop) { 813 | this._tooltip.scrollTop = tipOffsetTop; 814 | } 815 | } 816 | }, 817 | 818 | _handleSubmit: function() { //button and tooltip click and enter submit 819 | 820 | this._hideAutoType(); 821 | 822 | this.hideAlert(); 823 | this._hideTooltip(); 824 | 825 | if(this._input.style.display == 'none') //on first click show _input only 826 | this.expand(); 827 | else 828 | { 829 | if(this._input.value === '') //hide _input only 830 | this.collapse(); 831 | else 832 | { 833 | var loc = this._getLocation(this._input.value); 834 | 835 | if(loc===false) 836 | this.showAlert(); 837 | else 838 | { 839 | this.showLocation(loc, this._input.value); 840 | this.fire('search:locationfound', { 841 | latlng: loc, 842 | text: this._input.value, 843 | layer: loc.layer ? loc.layer : null 844 | }); 845 | } 846 | } 847 | } 848 | }, 849 | 850 | _getLocation: function(key) { //extract latlng from _recordsCache 851 | 852 | if( this._recordsCache.hasOwnProperty(key) ) 853 | return this._recordsCache[key];//then after use .loc attribute 854 | else 855 | return false; 856 | }, 857 | 858 | _defaultMoveToLocation: function(latlng, title, map) { 859 | if(this.options.zoom) 860 | this._map.setView(latlng, this.options.zoom); 861 | else 862 | this._map.panTo(latlng); 863 | }, 864 | 865 | showLocation: function(latlng, title) { //set location on map from _recordsCache 866 | var self = this; 867 | 868 | self._map.once('moveend zoomend', function(e) { 869 | 870 | if(self._markerSearch) { 871 | self._markerSearch.addTo(self._map).setLatLng(latlng); 872 | } 873 | 874 | }); 875 | 876 | self._moveToLocation(latlng, title, self._map); 877 | //FIXME autoCollapse option hide self._markerSearch before visualized!! 878 | if(self.options.autoCollapse) 879 | self.collapse(); 880 | 881 | return self; 882 | } 883 | }); 884 | 885 | L.Control.Search.Marker = L.Marker.extend({ 886 | 887 | includes: L.version[0]==='1' ? L.Evented.prototype : L.Mixin.Events, 888 | 889 | options: { 890 | icon: new L.Icon.Default(), 891 | animate: true, 892 | circle: { 893 | radius: 10, 894 | weight: 3, 895 | color: '#e03', 896 | stroke: true, 897 | fill: false 898 | } 899 | }, 900 | 901 | initialize: function (latlng, options) { 902 | L.setOptions(this, options); 903 | 904 | if(options.icon === true) 905 | options.icon = new L.Icon.Default(); 906 | 907 | L.Marker.prototype.initialize.call(this, latlng, options); 908 | 909 | if( L.Control.Search.prototype._isObject(this.options.circle) ) 910 | this._circleLoc = new L.CircleMarker(latlng, this.options.circle); 911 | }, 912 | 913 | onAdd: function (map) { 914 | L.Marker.prototype.onAdd.call(this, map); 915 | if(this._circleLoc) { 916 | map.addLayer(this._circleLoc); 917 | if(this.options.animate) 918 | this.animate(); 919 | } 920 | }, 921 | 922 | onRemove: function (map) { 923 | L.Marker.prototype.onRemove.call(this, map); 924 | if(this._circleLoc) 925 | map.removeLayer(this._circleLoc); 926 | }, 927 | 928 | setLatLng: function (latlng) { 929 | L.Marker.prototype.setLatLng.call(this, latlng); 930 | if(this._circleLoc) 931 | this._circleLoc.setLatLng(latlng); 932 | return this; 933 | }, 934 | 935 | _initIcon: function () { 936 | if(this.options.icon) 937 | L.Marker.prototype._initIcon.call(this); 938 | }, 939 | 940 | _removeIcon: function () { 941 | if(this.options.icon) 942 | L.Marker.prototype._removeIcon.call(this); 943 | }, 944 | 945 | animate: function() { 946 | //TODO refact animate() more smooth! like this: http://goo.gl/DDlRs 947 | if(this._circleLoc) 948 | { 949 | var circle = this._circleLoc, 950 | tInt = 200, //time interval 951 | ss = 5, //frames 952 | mr = parseInt(circle._radius/ss), 953 | oldrad = this.options.circle.radius, 954 | newrad = circle._radius * 2, 955 | acc = 0; 956 | 957 | circle._timerAnimLoc = setInterval(function() { 958 | acc += 0.5; 959 | mr += acc; //adding acceleration 960 | newrad -= mr; 961 | 962 | circle.setRadius(newrad); 963 | 964 | if(newrad