| @@ -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] | |||||