| @@ -0,0 +1,6 @@ | |||
| *.sqlite3 | |||
| *.swp | |||
| *.pyc | |||
| *.swo | |||
| env/ | |||
| */migrations/ | |||
| @@ -0,0 +1,126 @@ | |||
| """ | |||
| Django settings for bloomhunt project. | |||
| Generated by 'django-admin startproject' using Django 1.11. | |||
| For more information on this file, see | |||
| https://docs.djangoproject.com/en/1.11/topics/settings/ | |||
| For the full list of settings and their values, see | |||
| https://docs.djangoproject.com/en/1.11/ref/settings/ | |||
| """ | |||
| import os | |||
| # Build paths inside the project like this: os.path.join(BASE_DIR, ...) | |||
| BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||
| # Quick-start development settings - unsuitable for production | |||
| # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ | |||
| # SECURITY WARNING: keep the secret key used in production secret! | |||
| SECRET_KEY = '8$19!k4+%bg^r0v#04+ksm2ny(4*dp!v8dxnbbz)sj275(_(^&' | |||
| # SECURITY WARNING: don't run with debug turned on in production! | |||
| DEBUG = True | |||
| ALLOWED_HOSTS = [] | |||
| # Application definition | |||
| INSTALLED_APPS = [ | |||
| 'mobile', | |||
| 'map', | |||
| 'django.contrib.admin', | |||
| 'django.contrib.auth', | |||
| 'django.contrib.contenttypes', | |||
| 'django.contrib.sessions', | |||
| 'django.contrib.messages', | |||
| 'django.contrib.staticfiles', | |||
| ] | |||
| MIDDLEWARE = [ | |||
| 'django.middleware.security.SecurityMiddleware', | |||
| 'django.contrib.sessions.middleware.SessionMiddleware', | |||
| 'django.middleware.common.CommonMiddleware', | |||
| 'django.middleware.csrf.CsrfViewMiddleware', | |||
| 'django.contrib.auth.middleware.AuthenticationMiddleware', | |||
| 'django.contrib.messages.middleware.MessageMiddleware', | |||
| 'django.middleware.clickjacking.XFrameOptionsMiddleware', | |||
| ] | |||
| ROOT_URLCONF = 'bloomhunt.urls' | |||
| TEMPLATES = [ | |||
| { | |||
| 'BACKEND': 'django.template.backends.django.DjangoTemplates', | |||
| 'DIRS': [], | |||
| 'APP_DIRS': True, | |||
| 'OPTIONS': { | |||
| 'context_processors': [ | |||
| 'django.template.context_processors.debug', | |||
| 'django.template.context_processors.request', | |||
| 'django.contrib.auth.context_processors.auth', | |||
| 'django.contrib.messages.context_processors.messages', | |||
| ], | |||
| }, | |||
| }, | |||
| ] | |||
| WSGI_APPLICATION = 'bloomhunt.wsgi.application' | |||
| # Database | |||
| # https://docs.djangoproject.com/en/1.11/ref/settings/#databases | |||
| DATABASES = { | |||
| 'default': { | |||
| 'ENGINE': 'django.db.backends.sqlite3', | |||
| 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), | |||
| } | |||
| } | |||
| # Password validation | |||
| # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators | |||
| AUTH_PASSWORD_VALIDATORS = [ | |||
| { | |||
| 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | |||
| }, | |||
| { | |||
| 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | |||
| }, | |||
| { | |||
| 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | |||
| }, | |||
| { | |||
| 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | |||
| }, | |||
| ] | |||
| # Internationalization | |||
| # https://docs.djangoproject.com/en/1.11/topics/i18n/ | |||
| LANGUAGE_CODE = 'en-us' | |||
| TIME_ZONE = 'UTC' | |||
| USE_I18N = True | |||
| USE_L10N = True | |||
| USE_TZ = True | |||
| # Static files (CSS, JavaScript, Images) | |||
| # https://docs.djangoproject.com/en/1.11/howto/static-files/ | |||
| STATIC_URL = '/static/' | |||
| STATICFILES_DIRS = [ | |||
| os.path.join(BASE_DIR, "static"), | |||
| ] | |||
| @@ -0,0 +1,24 @@ | |||
| """bloomhunt URL Configuration | |||
| The `urlpatterns` list routes URLs to views. For more information please see: | |||
| https://docs.djangoproject.com/en/1.11/topics/http/urls/ | |||
| Examples: | |||
| Function views | |||
| 1. Add an import: from my_app import views | |||
| 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') | |||
| Class-based views | |||
| 1. Add an import: from other_app.views import Home | |||
| 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') | |||
| Including another URLconf | |||
| 1. Import the include() function: from django.conf.urls import url, include | |||
| 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) | |||
| """ | |||
| from django.conf.urls import url | |||
| from django.contrib import admin | |||
| from django.urls import include, path | |||
| urlpatterns = [ | |||
| url(r'^admin/', admin.site.urls), | |||
| url(r'^mobile/', include('mobile.urls')), | |||
| url(r'^map/', include('map.urls')) | |||
| ] | |||
| @@ -0,0 +1,16 @@ | |||
| """ | |||
| WSGI config for bloomhunt project. | |||
| It exposes the WSGI callable as a module-level variable named ``application``. | |||
| For more information on this file, see | |||
| https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ | |||
| """ | |||
| import os | |||
| from django.core.wsgi import get_wsgi_application | |||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bloomhunt.settings") | |||
| application = get_wsgi_application() | |||
| @@ -0,0 +1,22 @@ | |||
| #!/usr/bin/env python | |||
| import os | |||
| import sys | |||
| if __name__ == "__main__": | |||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bloomhunt.settings") | |||
| try: | |||
| from django.core.management import execute_from_command_line | |||
| except ImportError: | |||
| # The above import may fail for some other reason. Ensure that the | |||
| # issue is really that Django is missing to avoid masking other | |||
| # exceptions on Python 2. | |||
| try: | |||
| import django | |||
| except ImportError: | |||
| raise ImportError( | |||
| "Couldn't import Django. Are you sure it's installed and " | |||
| "available on your PYTHONPATH environment variable? Did you " | |||
| "forget to activate a virtual environment?" | |||
| ) | |||
| raise | |||
| execute_from_command_line(sys.argv) | |||
| @@ -0,0 +1,3 @@ | |||
| from django.contrib import admin | |||
| # Register your models here. | |||
| @@ -0,0 +1,5 @@ | |||
| from django.apps import AppConfig | |||
| class MapConfig(AppConfig): | |||
| name = 'map' | |||
| @@ -0,0 +1,3 @@ | |||
| from django.db import models | |||
| # Create your models here. | |||
| @@ -0,0 +1,43 @@ | |||
| #map1 { | |||
| height: 43% | |||
| } | |||
| #map2 { | |||
| margin-top: 10px; | |||
| height: 43%; | |||
| margin-bottom: 7px; | |||
| } | |||
| .slider { | |||
| -webkit-appearance: none; | |||
| width: 100%; | |||
| height: 15px; | |||
| border-radius: 5px; | |||
| background: #d3d3d3; | |||
| outline: none; | |||
| opacity: 0.7; | |||
| -webkit-transition: .2s; | |||
| transition: opacity .2s; | |||
| } | |||
| .slider::-webkit-slider-thumb { | |||
| -webkit-appearance: none; | |||
| appearance: none; | |||
| width: 25px; | |||
| height: 25px; | |||
| border-radius: 50%; | |||
| background: #4CAF50; | |||
| cursor: pointer; | |||
| } | |||
| .slider::-moz-range-thumb { | |||
| width: 25px; | |||
| height: 25px; | |||
| border-radius: 50%; | |||
| background: #4CAF50; | |||
| cursor: pointer; | |||
| } | |||
| .no-bloom-icon { | |||
| height: 30px; | |||
| } | |||
| @@ -0,0 +1,139 @@ | |||
| var p_date = document.getElementById("p_date"); | |||
| var map1 = L.map('map1').setView([49.4093524, 8.6931736], 15); | |||
| var map2 = L.map('map2').setView([49.4093524, 8.6931736], 15); | |||
| map1.sync(map2); | |||
| map2.sync(map1); | |||
| var tile = "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png " | |||
| var layer1 = L.tileLayer(tile, { | |||
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | |||
| }).addTo(map1); | |||
| var layer2 = L.tileLayer(tile, { | |||
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | |||
| }).addTo(map2); | |||
| //var layer1 = L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { | |||
| // attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>', | |||
| // maxZoom: 18, | |||
| // id: 'mapbox.streets', | |||
| // accessToken: 'pk.eyJ1IjoicG9zdGZsYXYiLCJhIjoiY2syN29xZHVpMnlzcDNtbXZkN2tlYWdhaSJ9.Th5tmsyOlPKoogGrOQrMNA' | |||
| //}); | |||
| //var layer2 = L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { | |||
| // attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>', | |||
| // maxZoom: 18, | |||
| // id: 'mapbox.hiking', | |||
| // accessToken: 'pk.eyJ1IjoicG9zdGZsYXYiLCJhIjoiY2syN29xZHVpMnlzcDNtbXZkN2tlYWdhaSJ9.Th5tmsyOlPKoogGrOQrMNA' | |||
| //}); | |||
| layer1.addTo(map1); | |||
| layer2.addTo(map2); | |||
| function mk_not_bloom_icon(label) { | |||
| return new L.DivIcon({ | |||
| className: 'no-bloom-icon', | |||
| html: '<img class="no-bloom-icon" src="/home/christian/repos/bloomMap2k19/bloomhunt/map/static/map/tree_not_blooming.png"/>' + | |||
| '<span>' + label + '</span>', | |||
| iconAnchor: [15, 30] | |||
| }); | |||
| } | |||
| function mk_bloom_icon(label) { | |||
| return new L.DivIcon({ | |||
| className: 'bloom-icon', | |||
| html: '<img class="no-bloom-icon" src="/home/christian/repos/bloomMap2k19/bloomhunt/map/static/map/tree_blooming.png"/>' + | |||
| '<span>' + label + '</span>', | |||
| iconAnchor: [15, 30] | |||
| }); | |||
| } | |||
| //var not_bloom_icon = L.icon({ | |||
| // iconUrl: 'tree_not_blooming.png', | |||
| // | |||
| // iconSize: [40, 40], // size of the icon | |||
| // shadowSize: [0, 0], // size of the shadow | |||
| // iconAnchor: [20, 40], // point of the icon which will correspond to marker's location | |||
| // shadowAnchor: [4, 62], // the same for the shadow | |||
| // popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor | |||
| //}); | |||
| //var bloom_icon = L.icon({ | |||
| // iconUrl: 'tree_blooming.png', | |||
| // | |||
| // iconSize: [40, 40], // size of the icon | |||
| // shadowSize: [0, 0], // size of the shadow | |||
| // iconAnchor: [20, 40], // point of the icon which will correspond to marker's location | |||
| // shadowAnchor: [4, 62], // the same for the shadow | |||
| // popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor | |||
| //}); | |||
| class Tree { | |||
| constructor(name, bloom_start, xcoord, ycoord) { | |||
| this.name = name; | |||
| this.bloom_start = bloom_start; | |||
| this.marker = L.marker([xcoord, ycoord], | |||
| {icon: mk_not_bloom_icon(this.name)}); | |||
| this.blooming = false; | |||
| } | |||
| update(dayno) { | |||
| if ((dayno < this.bloom_start || dayno >= this.bloom_start + 14) | |||
| && this.blooming) { | |||
| this.marker.setIcon(mk_not_bloom_icon(this.name)); | |||
| this.blooming = false; | |||
| } else if (dayno >= this.bloom_start | |||
| && dayno < this.bloom_start + 14 | |||
| && !this.blooming) { | |||
| this.marker.setIcon(mk_bloom_icon(this.name)); | |||
| this.blooming = true; | |||
| } | |||
| } | |||
| } | |||
| var tree = new Tree("Fritz", 55, 49.4093524, 8.6931736); | |||
| var tree2 = new Tree("Hans", 80, 45.02, 8.6931736); | |||
| var tree3 = new Tree("Hans", 80, 49.02, 8.69317); | |||
| var a_tree = new Tree("Fritz", 120, 49.4093524, 8.6931736); | |||
| var a_tree2 = new Tree("Hans", 100, 45.02, 8.6931736); | |||
| var a_tree3 = new Tree("Hans", 100, 49.02, 8.69317); | |||
| function updateMap(value) { | |||
| tree.update(value); | |||
| tree2.update(value); | |||
| tree3.update(value); | |||
| a_tree.update(value); | |||
| a_tree2.update(value); | |||
| a_tree3.update(value); | |||
| p_date.innerHTML = "Date: " + dateFromDay(value); | |||
| } | |||
| function dateFromDay(dayno) { | |||
| var date = new Date(2019, 0); // initialize a date in `year-01-01` | |||
| var date2 = new Date(date.setDate(dayno)); | |||
| return date2.getMonth() + 1 + "-" + date2.getDate(); | |||
| } | |||
| var cities1 = L.layerGroup([tree.marker, tree2.marker, tree3.marker]); | |||
| var cities2 = L.layerGroup([a_tree.marker, a_tree2.marker, a_tree3.marker]); | |||
| cities1.addTo(map1); | |||
| cities2.addTo(map2); | |||
| //var control1 = L.control.layers({'2019': cities1, '2020': cities1}, null, {collapsed: false}); | |||
| //var control2 = L.control.layers({'2019': cities2, '2020': cities2}, null, {collapsed: false}); | |||
| //control1.addTo(map1); | |||
| //control2.addTo(map2); | |||
| //map1.on('baselayerchange', function(val) { | |||
| // console.log(val); | |||
| //}); | |||
| // | |||
| //map2.on('baselayerchange', function(val) { | |||
| // console.log("map2", val); | |||
| //}); | |||
| @@ -0,0 +1,269 @@ | |||
| /* | |||
| * Extends L.Map to synchronize the interaction on one map to one or more other maps. | |||
| */ | |||
| (function () { | |||
| var NO_ANIMATION = { | |||
| animate: false, | |||
| reset: true, | |||
| disableViewprereset: true | |||
| }; | |||
| L.Sync = function () {}; | |||
| /* | |||
| * Helper function to compute the offset easily. | |||
| * | |||
| * The arguments are relative positions with respect to reference and target maps of | |||
| * the point to sync. If you provide ratioRef=[0, 1], ratioTarget=[1, 0] will sync the | |||
| * bottom left corner of the reference map with the top right corner of the target map. | |||
| * The values can be less than 0 or greater than 1. It will sync points out of the map. | |||
| */ | |||
| L.Sync.offsetHelper = function (ratioRef, ratioTarget) { | |||
| var or = L.Util.isArray(ratioRef) ? ratioRef : [0.5, 0.5]; | |||
| var ot = L.Util.isArray(ratioTarget) ? ratioTarget : [0.5, 0.5]; | |||
| return function (center, zoom, refMap, targetMap) { | |||
| var rs = refMap.getSize(); | |||
| var ts = targetMap.getSize(); | |||
| var pt = refMap.project(center, zoom) | |||
| .subtract([(0.5 - or[0]) * rs.x, (0.5 - or[1]) * rs.y]) | |||
| .add([(0.5 - ot[0]) * ts.x, (0.5 - ot[1]) * ts.y]); | |||
| return refMap.unproject(pt, zoom); | |||
| }; | |||
| }; | |||
| L.Map.include({ | |||
| sync: function (map, options) { | |||
| this._initSync(); | |||
| options = L.extend({ | |||
| noInitialSync: false, | |||
| syncCursor: false, | |||
| syncCursorMarkerOptions: { | |||
| radius: 10, | |||
| fillOpacity: 0.3, | |||
| color: '#da291c', | |||
| fillColor: '#fff' | |||
| }, | |||
| offsetFn: function (center, zoom, refMap, targetMap) { | |||
| // no transformation at all | |||
| return center; | |||
| } | |||
| }, options); | |||
| // prevent double-syncing the map: | |||
| if (this._syncMaps.indexOf(map) === -1) { | |||
| this._syncMaps.push(map); | |||
| this._syncOffsetFns[L.Util.stamp(map)] = options.offsetFn; | |||
| } | |||
| if (!options.noInitialSync) { | |||
| map.setView( | |||
| options.offsetFn(this.getCenter(), this.getZoom(), this, map), | |||
| this.getZoom(), NO_ANIMATION); | |||
| } | |||
| if (options.syncCursor) { | |||
| if (typeof map.cursor === 'undefined') { | |||
| map.cursor = L.circleMarker([0, 0], options.syncCursorMarkerOptions).addTo(map); | |||
| } | |||
| this._cursors.push(map.cursor); | |||
| this.on('mousemove', this._cursorSyncMove, this); | |||
| this.on('mouseout', this._cursorSyncOut, this); | |||
| } | |||
| // on these events, we should reset the view on every synced map | |||
| // dragstart is due to inertia | |||
| this.on('resize zoomend', this._selfSetView); | |||
| this.on('moveend', this._syncOnMoveend); | |||
| this.on('dragend', this._syncOnDragend); | |||
| return this; | |||
| }, | |||
| // unsync maps from each other | |||
| unsync: function (map) { | |||
| var self = this; | |||
| if (this._cursors) { | |||
| this._cursors.forEach(function (cursor, indx, _cursors) { | |||
| if (cursor === map.cursor) { | |||
| _cursors.splice(indx, 1); | |||
| } | |||
| }); | |||
| } | |||
| // TODO: hide cursor in stead of moving to 0, 0 | |||
| if (map.cursor) { | |||
| map.cursor.setLatLng([0, 0]); | |||
| } | |||
| if (this._syncMaps) { | |||
| this._syncMaps.forEach(function (synced, id) { | |||
| if (map === synced) { | |||
| delete self._syncOffsetFns[L.Util.stamp(map)]; | |||
| self._syncMaps.splice(id, 1); | |||
| } | |||
| }); | |||
| } | |||
| if (!this._syncMaps || this._syncMaps.length == 0) { | |||
| // no more synced maps, so these events are not needed. | |||
| this.off('resize zoomend', this._selfSetView); | |||
| this.off('moveend', this._syncOnMoveend); | |||
| this.off('dragend', this._syncOnDragend); | |||
| } | |||
| return this; | |||
| }, | |||
| // Checks if the map is synced with anything or a specifyc map | |||
| isSynced: function (otherMap) { | |||
| var has = (this.hasOwnProperty('_syncMaps') && Object.keys(this._syncMaps).length > 0); | |||
| if (has && otherMap) { | |||
| // Look for this specific map | |||
| has = false; | |||
| this._syncMaps.forEach(function (synced) { | |||
| if (otherMap == synced) { has = true; } | |||
| }); | |||
| } | |||
| return has; | |||
| }, | |||
| // Callbacks for events... | |||
| _cursorSyncMove: function (e) { | |||
| this._cursors.forEach(function (cursor) { | |||
| cursor.setLatLng(e.latlng); | |||
| }); | |||
| }, | |||
| _cursorSyncOut: function (e) { | |||
| this._cursors.forEach(function (cursor) { | |||
| // TODO: hide cursor in stead of moving to 0, 0 | |||
| cursor.setLatLng([0, 0]); | |||
| }); | |||
| }, | |||
| _selfSetView: function (e) { | |||
| // reset the map, and let setView synchronize the others. | |||
| this.setView(this.getCenter(), this.getZoom(), NO_ANIMATION); | |||
| }, | |||
| _syncOnMoveend: function (e) { | |||
| if (this._syncDragend) { | |||
| // This is 'the moveend' after the dragend. | |||
| // Without inertia, it will be right after, | |||
| // but when inertia is on, we need this to detect that. | |||
| this._syncDragend = false; // before calling setView! | |||
| this._selfSetView(e); | |||
| this._syncMaps.forEach(function (toSync) { | |||
| toSync.fire('moveend'); | |||
| }); | |||
| } | |||
| }, | |||
| _syncOnDragend: function (e) { | |||
| // It is ugly to have state, but we need it in case of inertia. | |||
| this._syncDragend = true; | |||
| }, | |||
| // overload methods on originalMap to replay interactions on _syncMaps; | |||
| _initSync: function () { | |||
| if (this._syncMaps) { | |||
| return; | |||
| } | |||
| var originalMap = this; | |||
| this._syncMaps = []; | |||
| this._cursors = []; | |||
| this._syncOffsetFns = {}; | |||
| L.extend(originalMap, { | |||
| setView: function (center, zoom, options, sync) { | |||
| // Use this sandwich to disable and enable viewprereset | |||
| // around setView call | |||
| function sandwich (obj, fn) { | |||
| var viewpreresets = []; | |||
| var doit = options && options.disableViewprereset && obj && obj._events; | |||
| if (doit) { | |||
| // The event viewpreresets does an invalidateAll, | |||
| // that reloads all the tiles. | |||
| // That causes an annoying flicker. | |||
| viewpreresets = obj._events.viewprereset; | |||
| obj._events.viewprereset = []; | |||
| } | |||
| var ret = fn(obj); | |||
| if (doit) { | |||
| // restore viewpreresets event to its previous values | |||
| obj._events.viewprereset = viewpreresets; | |||
| } | |||
| return ret; | |||
| } | |||
| // Looks better if the other maps 'follow' the active one, | |||
| // so call this before _syncMaps | |||
| var ret = sandwich(this, function (obj) { | |||
| return L.Map.prototype.setView.call(obj, center, zoom, options); | |||
| }); | |||
| if (!sync) { | |||
| originalMap._syncMaps.forEach(function (toSync) { | |||
| sandwich(toSync, function (obj) { | |||
| return toSync.setView( | |||
| originalMap._syncOffsetFns[L.Util.stamp(toSync)](center, zoom, originalMap, toSync), | |||
| zoom, options, true); | |||
| }); | |||
| }); | |||
| } | |||
| return ret; | |||
| }, | |||
| panBy: function (offset, options, sync) { | |||
| if (!sync) { | |||
| originalMap._syncMaps.forEach(function (toSync) { | |||
| toSync.panBy(offset, options, true); | |||
| }); | |||
| } | |||
| return L.Map.prototype.panBy.call(this, offset, options); | |||
| }, | |||
| _onResize: function (event, sync) { | |||
| if (!sync) { | |||
| originalMap._syncMaps.forEach(function (toSync) { | |||
| toSync._onResize(event, true); | |||
| }); | |||
| } | |||
| return L.Map.prototype._onResize.call(this, event); | |||
| }, | |||
| _stop: function (sync) { | |||
| L.Map.prototype._stop.call(this); | |||
| if (!sync) { | |||
| originalMap._syncMaps.forEach(function (toSync) { | |||
| toSync._stop(true); | |||
| }); | |||
| } | |||
| } | |||
| }); | |||
| originalMap.dragging._draggable._updatePosition = function () { | |||
| L.Draggable.prototype._updatePosition.call(this); | |||
| var self = this; | |||
| originalMap._syncMaps.forEach(function (toSync) { | |||
| L.DomUtil.setPosition(toSync.dragging._draggable._element, self._newPos); | |||
| toSync.eachLayer(function (layer) { | |||
| if (layer._google !== undefined) { | |||
| var offsetFn = originalMap._syncOffsetFns[L.Util.stamp(toSync)]; | |||
| var center = offsetFn(originalMap.getCenter(), originalMap.getZoom(), originalMap, toSync); | |||
| layer._google.setCenter(center); | |||
| } | |||
| }); | |||
| toSync.fire('move'); | |||
| }); | |||
| }; | |||
| } | |||
| }); | |||
| })(); | |||
| @@ -0,0 +1,177 @@ | |||
| {% load static %} | |||
| <html> | |||
| <head> | |||
| <title>Bloommap</title> | |||
| <link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/> | |||
| <link rel="stylesheet" href="{% static 'map/index.css' %}"> | |||
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" | |||
| integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" | |||
| crossorigin=""/> | |||
| <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js" | |||
| integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og==" | |||
| crossorigin=""></script> | |||
| <script src="{% static 'map/mapsync.js' %}"></script> | |||
| </head> | |||
| <body> | |||
| <div id="map1"></div> | |||
| <div id="map2"></div> | |||
| <div style="pointer-events: none; position: absolute; top: 30px; right: 40px; z-index: 1000">2019</div> | |||
| <div style="pointer-events: none; position: absolute; top: 310px; right: 40px; z-index: 1001">2029</div> | |||
| <div class="slidecontainer"> | |||
| <input type="range" min="1" max="365" value="180" class="slider" id="myRange" | |||
| oninput="updateMap(this.value)"> | |||
| </div> | |||
| <p id="p_date"></p> | |||
| <script> | |||
| var p_date = document.getElementById("p_date"); | |||
| var map1 = L.map('map1').setView([49.4093524, 8.6931736], 15); | |||
| var map2 = L.map('map2').setView([49.4093524, 8.6931736], 15); | |||
| map1.sync(map2); | |||
| map2.sync(map1); | |||
| var tile = "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png " | |||
| //var layer1 = L.tileLayer(tile, { | |||
| // attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | |||
| //}).addTo(map1); | |||
| // | |||
| //var layer2 = L.tileLayer(tile, { | |||
| // attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' | |||
| //}).addTo(map2); | |||
| var layer1 = L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { | |||
| attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>', | |||
| maxZoom: 18, | |||
| id: 'mapbox.streets', | |||
| accessToken: 'pk.eyJ1IjoicG9zdGZsYXYiLCJhIjoiY2syN29xZHVpMnlzcDNtbXZkN2tlYWdhaSJ9.Th5tmsyOlPKoogGrOQrMNA' | |||
| }); | |||
| var layer2 = L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { | |||
| attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>', | |||
| maxZoom: 18, | |||
| id: 'mapbox.streets', | |||
| accessToken: 'pk.eyJ1IjoicG9zdGZsYXYiLCJhIjoiY2syN29xZHVpMnlzcDNtbXZkN2tlYWdhaSJ9.Th5tmsyOlPKoogGrOQrMNA' | |||
| }); | |||
| layer1.addTo(map1); | |||
| layer2.addTo(map2); | |||
| function mk_not_bloom_icon(label) { | |||
| return new L.DivIcon({ | |||
| className: 'no-bloom-icon', | |||
| html: '<img class="no-bloom-icon" src="{% static 'map/tree_not_blooming.png' %}"/>' + | |||
| '<span>' + label + '</span>', | |||
| iconAnchor: [15, 30] | |||
| }); | |||
| } | |||
| function mk_bloom_icon(label) { | |||
| return new L.DivIcon({ | |||
| className: 'bloom-icon', | |||
| html: '<img class="no-bloom-icon" src="{% static 'map/tree_blooming.png' %}"/>' + | |||
| '<span>' + label + '</span>', | |||
| iconAnchor: [15, 30] | |||
| }); | |||
| } | |||
| //var not_bloom_icon = L.icon({ | |||
| // iconUrl: 'tree_not_blooming.png', | |||
| // | |||
| // iconSize: [40, 40], // size of the icon | |||
| // shadowSize: [0, 0], // size of the shadow | |||
| // iconAnchor: [20, 40], // point of the icon which will correspond to marker's location | |||
| // shadowAnchor: [4, 62], // the same for the shadow | |||
| // popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor | |||
| //}); | |||
| //var bloom_icon = L.icon({ | |||
| // iconUrl: 'tree_blooming.png', | |||
| // | |||
| // iconSize: [40, 40], // size of the icon | |||
| // shadowSize: [0, 0], // size of the shadow | |||
| // iconAnchor: [20, 40], // point of the icon which will correspond to marker's location | |||
| // shadowAnchor: [4, 62], // the same for the shadow | |||
| // popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor | |||
| //}); | |||
| class Tree { | |||
| constructor(name, bloom_start, xcoord, ycoord) { | |||
| this.name = name; | |||
| this.bloom_start = bloom_start; | |||
| this.marker = L.marker([xcoord, ycoord], | |||
| {icon: mk_not_bloom_icon(this.name)}); | |||
| this.blooming = false; | |||
| } | |||
| update(dayno) { | |||
| if ((dayno < this.bloom_start || dayno >= this.bloom_start + 14) | |||
| && this.blooming) { | |||
| this.marker.setIcon(mk_not_bloom_icon(this.name)); | |||
| this.blooming = false; | |||
| } else if (dayno >= this.bloom_start | |||
| && dayno < this.bloom_start + 14 | |||
| && !this.blooming) { | |||
| this.marker.setIcon(mk_bloom_icon(this.name)); | |||
| this.blooming = true; | |||
| } | |||
| } | |||
| } | |||
| var treesFromJSON = {{ trees|safe }}["trees"]; | |||
| var trees_1 = []; | |||
| for (var i = 0; i < treesFromJSON.length; i++) { | |||
| let raw = treesFromJSON[i]; | |||
| let tree = new Tree(raw["name"], raw["bloom_start"], | |||
| raw["latitude"], raw["longitude"]); | |||
| trees_1.push(tree); | |||
| } | |||
| var trees_2 = []; | |||
| for (var i = 0; i < treesFromJSON.length; i++) { | |||
| let raw = treesFromJSON[i]; | |||
| let tree = new Tree(raw["name"], raw["bloom_start"] + 20, | |||
| raw["latitude"], raw["longitude"]); | |||
| trees_2.push(tree); | |||
| } | |||
| function updateMap(value) { | |||
| for (var i = 0; i < trees_1.length; i++) { | |||
| trees_1[i].update(value); | |||
| } | |||
| for (var i = 0; i < trees_2.length; i++) { | |||
| trees_2[i].update(value); | |||
| } | |||
| p_date.innerHTML = "Date: " + dateFromDay(value); | |||
| } | |||
| function dateFromDay(dayno) { | |||
| var date = new Date(2019, 0); // initialize a date in `year-01-01` | |||
| var date2 = new Date(date.setDate(dayno)); | |||
| return date2.getMonth() + 1 + "-" + date2.getDate(); | |||
| } | |||
| var cities1 = L.layerGroup(trees_1.map(t => t.marker)); | |||
| var cities2 = L.layerGroup(trees_2.map(t => t.marker)); | |||
| cities1.addTo(map1); | |||
| cities2.addTo(map2); | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,3 @@ | |||
| from django.test import TestCase | |||
| # Create your tests here. | |||
| @@ -0,0 +1,7 @@ | |||
| from django.urls import path | |||
| from . import views | |||
| urlpatterns = [ | |||
| path('', views.index, name='index') | |||
| ] | |||
| @@ -0,0 +1,15 @@ | |||
| from django.http import HttpResponse | |||
| from django.shortcuts import render | |||
| import json | |||
| import os | |||
| def index(request): | |||
| if not os.path.isfile("/tmp/data.json"): | |||
| data = {"trees": str({ "trees": []}) } | |||
| else: | |||
| with open("/tmp/data.json", "r") as f: | |||
| loaded = json.load(f) | |||
| s = json.dumps(loaded); | |||
| data = {"trees": s} | |||
| return render(request, "map/index.html", context=data); | |||
| @@ -0,0 +1,3 @@ | |||
| from django.contrib import admin | |||
| # Register your models here. | |||
| @@ -0,0 +1,5 @@ | |||
| from django.apps import AppConfig | |||
| class MobileConfig(AppConfig): | |||
| name = 'mobile' | |||
| @@ -0,0 +1,3 @@ | |||
| from django.db import models | |||
| # Create your models here. | |||
| @@ -0,0 +1,18 @@ | |||
| <html> | |||
| <head> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous"> | |||
| </head> | |||
| <body> | |||
| <div id=content> | |||
| {% block content %} | |||
| {% endblock %} | |||
| </div> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,10 @@ | |||
| {% extends "mobile/base.html" %} | |||
| {% block content %} | |||
| <body> | |||
| <h1>Bloomhunt</h1> | |||
| <p>Hunt blooming trees in your area.</p> | |||
| <a class="btn btn-default" id="btn_scan" href="/mobile/scan">Scan a blooming tree</a> | |||
| {% endblock %} | |||
| @@ -0,0 +1,19 @@ | |||
| {% extends "mobile/base.html" %} | |||
| {% block content %} | |||
| <h1>Scan a blooming tree!</h1> | |||
| <p>We will help you identifying the tree you are hunting.</p> | |||
| <p>{{ question.text }}</p> | |||
| {% for option in question.options %} | |||
| <form action="" method="post"> | |||
| {% csrf_token %} | |||
| <input name="question" type="hidden" value="{{ question.key }}"> | |||
| <input name="chosen_option" class="btn btn-default" type="submit" value="{{ option.text }}"> | |||
| </form> | |||
| {% endfor %} | |||
| {% endblock %} | |||
| @@ -0,0 +1,28 @@ | |||
| {% extends "mobile/base.html" %} | |||
| {% block content %} | |||
| <p>Congratulions, you are the first to report that this <b>{{ tree.name }}</b> | |||
| tree is blooming.<p> | |||
| <p>You may give it a name now</p> | |||
| <form action="/mobile/addtree/" method="post"> | |||
| {% csrf_token %} | |||
| <input type="text" name="name"> | |||
| <input type="hidden" name="tree" value="{{ tree.name }}"> | |||
| <input id="in_long" type="text" name="longitude"> | |||
| <input id="in_lat" type="text" name="latitude"> | |||
| <input type="submit" value="Name your tree"> | |||
| </form> | |||
| <script> | |||
| navigator.geolocation.getCurrentPosition(function(position) { | |||
| var in_long = document.getElementById("in_long"); | |||
| var in_lat = document.getElementById("in_lat"); | |||
| in_long.value = position.coords.longitude; | |||
| in_lat.value = position.coords.latitude; | |||
| }); | |||
| </script> | |||
| {% endblock %} | |||
| @@ -0,0 +1,12 @@ | |||
| {% extends "mobile/base.html" %} | |||
| {% block content %} | |||
| <h1>You are now one of the glorious Bloom Hunters!</h1> | |||
| <p>Does that feel great? It should! You really achieved something in | |||
| your life!</p> | |||
| <a class="btn btn-default" href="/mobile">Back to hunting</a> | |||
| {% endblock %} | |||
| @@ -0,0 +1,3 @@ | |||
| from django.test import TestCase | |||
| # Create your tests here. | |||
| @@ -0,0 +1,9 @@ | |||
| from django.urls import path | |||
| from . import views | |||
| urlpatterns = [ | |||
| path('addtree/', views.addtree, name='addtree'), | |||
| path('scan/', views.scan , name='scan'), | |||
| path('', views.index, name='index') | |||
| ] | |||
| @@ -0,0 +1,88 @@ | |||
| from django.http import HttpResponse, HttpResponseRedirect | |||
| from django.shortcuts import render | |||
| from datetime import datetime | |||
| import json | |||
| import os | |||
| def index(request): | |||
| return render(request, 'mobile/index.html') | |||
| def scan(request): | |||
| question = q1 | |||
| if request.POST: | |||
| data = request.POST | |||
| print("data") | |||
| if data["question"]: | |||
| print("response to question", data["question"]) | |||
| for q in questions: | |||
| if not q.key == data["question"]: | |||
| continue | |||
| for opt in q.options: | |||
| if not opt.text == data["chosen_option"]: | |||
| continue | |||
| if opt.question: | |||
| question = opt.question | |||
| break | |||
| else: | |||
| return scanned(request, opt.result) | |||
| # first question | |||
| context = {'question': question} | |||
| return render(request, 'mobile/scan.html', context) | |||
| def scanned(request, tree): | |||
| context = {'tree': {'name': tree}} | |||
| return render(request, 'mobile/scanned.html', context) | |||
| def addtree(request): | |||
| if not request.POST: | |||
| return HttpResponseRedirect("/mobile/scan") | |||
| name = request.POST["name"] | |||
| tree = request.POST["name"] | |||
| longitude = request.POST["longitude"] | |||
| latitude = request.POST["latitude"] | |||
| bloom_start = datetime.now().timetuple().tm_yday | |||
| if not os.path.isfile('/tmp/data.json'): | |||
| data = {'trees': []} | |||
| else: | |||
| with open('/tmp/data.json', 'r') as f: | |||
| data = json.load(f) | |||
| with open('/tmp/data.json', 'w') as f: | |||
| data["trees"].append( | |||
| {'name': name, 'species': tree, | |||
| 'longitude': longitude, 'latitude': latitude, | |||
| 'bloom_start': bloom_start } | |||
| ) | |||
| json.dump(data, f, indent=4) | |||
| return render(request, "mobile/thankyou.html") | |||
| class Option: | |||
| def __init__(self, text, question=None, result=None): | |||
| self.text = text | |||
| self.question = question | |||
| self.result = result | |||
| class Question: | |||
| def __init__(self, key, text, options): | |||
| self.key = key | |||
| self.text = text | |||
| self.options = options | |||
| q3 = Question("alive", "Is your tree alive?", | |||
| [Option("yes", result="Poor Apple Tree"), | |||
| Option("no", result="Dead Apple Tree")]) | |||
| q2 = Question("red", "Is your tree red?", | |||
| [Option("yes", result="Red Apple Tree"), | |||
| Option("no", result="Green Apple Tree")]) | |||
| q1 = Question("leaves", "Does your tree have leaves?", | |||
| [Option("yes", question=q2), Option("no", question=q3)]) | |||
| questions = [q1, q2, q3] | |||