diff --git a/.gitignore b/.gitignore index 95618e7c..bf1c6e05 100755 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,16 @@ bazel-* test-project-host-* dist/ dist-schema/ +var/ #doc -documentation \ No newline at end of file +documentation +geopaysagesftpclient/geopaysagesftpclient/tests/pt_200_400.jpg~ +geopaysagesftpclient/geopaysagesftpclient/tests/pt_300_500.jpg~ +geopaysagesftpclient/geopaysagesftpclient/tests/pt_400_600.jpg~ +geopaysagesftpclient/config.ini +geopaysagesftpclient/pytest.ini +geopaysagesftpclient/.vscode/ +geopaysagesftpclient/geopaysagesftpclient.egg-info/ +geopaysagesftpclient/output/ diff --git a/README.md b/README.md index 62dbe2a0..2ba9050e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Application web permettant de publier un observatoire photographique des paysages. ## Install +Documentation d'installation : https://github.com/PnX-SI/GeoPaysages/tree/master/docs/installation.rst - Créer et activer un environnement virtuel python 3. - cd ./backend @@ -23,8 +24,15 @@ Le projet est porté par le Parc national de la Vanoise. Le site qui sera dével A suivre... +![alt text](./docs/screenshot.jpg) + ## Présentation - CCTP 2017 : http://geonature.fr/documents/autres/geopaysages/2017-11-13-CDC-OPPV-PNV.pdf - Annexe CCTP 2017 : http://geonature.fr/documents/autres/geopaysages/2017-11-24-OPPV-PNV-ANNEXES-CDC.zip - Réflexion 2016 : http://geonature.fr/documents/autres/geopaysages/2016-11-OPP-reflexion.pdf + +## DEMO + +http://5.196.209.137/ + diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..e7526496 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.1.dev0 diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 00000000..615aafb0 --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/usr/bin/python3" +} \ No newline at end of file diff --git a/backend/api.py b/backend/api.py index 4934635c..2d0471e5 100755 --- a/backend/api.py +++ b/backend/api.py @@ -1,8 +1,9 @@ -from flask import Flask, request, Blueprint, jsonify, url_for +from flask import Flask, request, Blueprint, Response, jsonify, url_for from routes import main as main_blueprint -from config import DATA_IMAGES_PATH +from config import DATA_IMAGES_PATH, DATA_NOTICES_PATH from pypnusershub import routes as fnauth - +import fnmatch +import re from pypnusershub import routes import models import json @@ -20,20 +21,85 @@ licences_schema = models.LicencePhotoSchema(many=True) users_schema = user_models.usersViewSchema(many=True) corThemeStheme_Schema = models.CorThemeSthemeSchema(many=True) +themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) +ville_schema = models.VilleSchema(many=True) @api.route('/api/sites', methods=['GET']) def returnAllSites(): - get_all_sites = models.TSite.query.all() - sites = site_schema.dump(get_all_sites).data - for site in sites: - get_photo = models.TPhoto.query.filter_by( - id_photo=site.get('t_photos')[0]) - main_photo = photo_schema.dump(get_photo).data - site['main_photo'] = utils.getThumbnail( - main_photo[0]).get('output_name') + try: + get_all_sites = models.TSite.query.order_by('ref_site').all() + sites = site_schema.dump(get_all_sites).data + for site in sites: + if(site.get('main_photo') == None): + set_main_photo = models.TPhoto.query.filter_by( + id_photo=site.get('t_photos')[0]) + else: + set_main_photo = models.TPhoto.query.filter_by( + id_photo=site.get('main_photo')) + ''' + get_photos = models.TPhoto.query.filter( + models.TPhoto.id_photo.in_(site.get('t_photos'))) + dump_photos = photo_schema.dump(get_photos).data + for photo in dump_photos : + photo['sm'] = utils.getThumbnail(photo).get('output_name'), + site['photos'] = dump_photos + ''' + main_photo = photo_schema.dump(set_main_photo).data + site['main_photo'] = utils.getThumbnail( + main_photo[0]).get('output_name') + except Exception as exception: + return jsonify(error=exception), 400 return jsonify(sites), 200 - + + +@api.route('/api/site/', methods=['GET']) +def returnSiteById(id_site): + try: + get_site_by_id = models.TSite.query.filter_by(id_site=id_site) + site = site_schema.dump(get_site_by_id).data + get_photos_by_site = models.TPhoto.query.order_by( + 'filter_date').filter_by(id_site=id_site).all() + dump_photos = photo_schema.dump(get_photos_by_site).data + + cor_sthemes_themes = site[0].get('cor_site_stheme_themes') + cor_list = [] + themes_list = [] + subthemes_list = [] + for cor in cor_sthemes_themes: + cor_list.append(cor.get('id_stheme_theme')) + query = models.CorSthemeTheme.query.filter( + models.CorSthemeTheme.id_stheme_theme.in_(cor_list)) + themes_sthemes = themes_sthemes_schema.dump(query).data + + for item in themes_sthemes: + if item.get('dico_theme').get('id_theme') not in themes_list: + themes_list.append(item.get('dico_theme').get('id_theme')) + if item.get('dico_stheme').get('id_stheme') not in subthemes_list: + subthemes_list.append(item.get('dico_stheme').get('id_stheme')) + + site[0]['themes'] = themes_list + site[0]['subthemes'] = subthemes_list + + for photo in dump_photos: + photo['sm'] = utils.getThumbnail(photo).get('output_name'), + + photos = dump_photos + except Exception as exception: + return jsonify(error=exception), 400 + return jsonify(site=site, photos=photos), 200 + + +@api.route('/api/gallery', methods=['GET']) +def gallery(): + try: + get_photos = models.TPhoto.query.order_by('id_site').all() + dump_photos = photo_schema.dump(get_photos).data + for photo in dump_photos: + photo['sm'] = utils.getThumbnail(photo).get('output_name'), + except Exception as exception: + return jsonify(error=exception), 400 + return jsonify(dump_photos), 200 @api.route('/api/themes', methods=['GET']) @@ -41,9 +107,9 @@ def returnAllThemes(): try: get_all_themes = models.DicoTheme.query.all() themes = themes_schema.dump(get_all_themes).data - return jsonify(themes), 200 except Exception as exception: return jsonify(error=exception), 400 + return jsonify(themes), 200 @api.route('/api/subThemes', methods=['GET']) @@ -57,9 +123,9 @@ def returnAllSubthemes(): themes_of_subthemes.append(item.get('id_theme')) sub['themes'] = themes_of_subthemes del sub['cor_stheme_themes'] - return jsonify(subthemes), 200 except Exception as exception: return jsonify(error=exception), 400 + return jsonify(subthemes), 200 @api.route('/api/licences', methods=['GET']) @@ -67,38 +133,90 @@ def returnAllLicences(): try: get_all_licences = models.DicoLicencePhoto.query.all() licences = licences_schema.dump(get_all_licences).data - return jsonify(licences), 200 except Exception as exception: return jsonify(error=exception), 400 + return jsonify(licences), 200 -@api.route('/api/users', methods=['GET']) -def returnAllUsers(): +@api.route('/api/users/', methods=['GET']) +def returnAllUsers(id_app): try: - get_all_users = user_models.UsersView.query.all() + get_all_users = user_models.UsersView.query.filter_by( + id_application=id_app).all() users = users_schema.dump(get_all_users).data - return jsonify(users), 200 except Exception as exception: return jsonify(error=exception), 400 + return jsonify(users), 200 -@api.route('/api/addSite', methods=['POST']) +@api.route('/api/me/', methods=['GET']) +@fnauth.check_auth(2, True, None, None) +def returnCurrentUser(id_role=None): + try: + get_current_user = user_models.UsersView.query.filter_by( + id_role=id_role).all() + current_user = users_schema.dump(get_current_user).data + except Exception as exception: + return jsonify(error=exception), 400 + return jsonify(current_user), 200 + + +@api.route('/api/site/', methods=['DELETE']) @fnauth.check_auth(6, False, None, None) +def deleteSite(id_site): + base_path = './static/' + DATA_IMAGES_PATH + try: + models.CorSiteSthemeTheme.query.filter_by(id_site=id_site).delete() + photos = models.TPhoto.query.filter_by(id_site=id_site).all() + photos = photo_schema.dump(photos).data + models.TPhoto.query.filter_by(id_site=id_site).delete() + site = models.TSite.query.filter_by(id_site=id_site).delete() + for photo in photos: + photo_name = photo.get('path_file_photo') + for fileName in os.listdir(base_path): + if fileName.endswith(photo_name): + os.remove(base_path + fileName) + db.session.commit() + except Exception as exception: + return jsonify(error=exception), 400 + if site: + return jsonify('site has been deleted'), 200 + else: + return jsonify('error'), 400 + + +@api.route('/api/addSite', methods=['POST']) +@fnauth.check_auth(2, False, None, None) def add_site(): try: data = dict(request.get_json()) site = models.TSite(**data) db.session.add(site) db.session.commit() - return jsonify(id_site=site.id_site), 200 except Exception as exception: - return jsonify(error=exception), 400 + return (exception), 400 + return jsonify(id_site=site.id_site), 200 + + +@api.route('/api/updateSite', methods=['PATCH']) +@fnauth.check_auth(2, False, None, None) +def update_site(): + site = request.get_json() + try: + models.CorSiteSthemeTheme.query.filter_by( + id_site=site.get('id_site')).delete() + models.TSite.query.filter_by(id_site=site.get('id_site')).update(site) + db.session.commit() + except Exception as exception: + return (exception), 400 + return jsonify('site updated successfully'), 200 @api.route('/api/addThemes', methods=['POST']) +@fnauth.check_auth(2, False, None, None) def add_cor_site_theme_stheme(): + data = request.get_json().get('data') try: - data = request.get_json().get('data') for d in data: get_id_stheme_theme = models.CorSthemeTheme.query.filter_by( id_theme=d.get('id_theme'), id_stheme=d.get('id_stheme')).all() @@ -108,28 +226,151 @@ def add_cor_site_theme_stheme(): site_theme_stheme = models.CorSiteSthemeTheme(**id_stheme_theme[0]) db.session.add(site_theme_stheme) db.session.commit() - return jsonify('success'), 200 except Exception as exception: - return jsonify(error=exception), 400 + return (exception), 400 + return jsonify('success'), 200 @api.route('/api/addPhotos', methods=['POST']) +@fnauth.check_auth(2, False, None, None) def upload_file(): base_path = './static/' + DATA_IMAGES_PATH data = request.form.getlist('data') + new_site = request.form.getlist('new_site') uploaded_images = request.files.getlist('image') - for d in data: - d_serialized = json.loads(d) - check_exist = models.TPhoto.query.filter_by( - path_file_photo=d_serialized.get('path_file_photo')).first() - if(check_exist): - models.TSite.query.filter_by(id_site=d_serialized.get('id_site')).delete() + try: + for d in data: + d_serialized = json.loads(d) + check_exist = models.TPhoto.query.filter_by( + path_file_photo=d_serialized.get('path_file_photo')).first() + if(check_exist): + if (new_site == 'true'): + models.TSite.query.filter_by( + id_site=d_serialized.get('id_site')).delete() + models.CorSiteSthemeTheme.query.filter_by( + id_site=d_serialized.get('id_site')).delete() + db.session.commit() + return jsonify(error='image_already_exist', image=d_serialized.get('path_file_photo')), 400 + main_photo = d_serialized.get('main_photo') + del d_serialized['main_photo'] + photo = models.TPhoto(**d_serialized) + db.session.add(photo) db.session.commit() - return jsonify(error='image_already_exist', image=d_serialized.get('path_file_photo')), 400 - photo = models.TPhoto(**d_serialized) - db.session.add(photo) + if (main_photo == True): + photos_query = models.TPhoto.query.filter_by( + path_file_photo=d_serialized.get('path_file_photo')).all() + photo_id = photo_schema.dump( + photos_query).data[0].get('id_photo') + models.TSite.query.filter_by(id_site=d_serialized.get( + 'id_site')).update({models.TSite.main_photo: photo_id}) + db.session.commit() + for image in uploaded_images: + image.save(os.path.join(base_path + image.filename)) + except Exception as exception: + return (exception), 400 + return jsonify('photo added successfully'), 200 + + +@api.route('/api/addNotices', methods=['POST']) +@fnauth.check_auth(2, False, None, None) +def upload_notice(): + try: + base_path = './static/' + DATA_NOTICES_PATH + notice = request.files.get('notice') + notice.save(os.path.join(base_path + notice.filename)) + except Exception as exception: + return (exception), 400 + return jsonify('notice added successfully'), 200 + + +@api.route('/api/deleteNotice/', methods=['DELETE']) +@fnauth.check_auth(2, False, None, None) +def delete_notice(notice): + base_path = './static/' + DATA_NOTICES_PATH + try: + for fileName in os.listdir(base_path): + if (fileName == notice): + os.remove(base_path + fileName) + except Exception as exception: + return (exception), 400 + return jsonify('notice removed successfully'), 200 + + +@api.route('/api/updatePhoto', methods=['PATCH']) +@fnauth.check_auth(2, False, None, None) +def update_photo(): + base_path = './static/' + DATA_IMAGES_PATH + data = request.form.get('data') + image = request.files.get('image') + data_serialized = json.loads(data) + try: + photos_query = models.TPhoto.query.filter_by( + id_photo=data_serialized.get('id_photo')).all() + photo_name = photo_schema.dump( + photos_query).data[0].get('path_file_photo') + if (data_serialized.get('main_photo') == True): + models.TSite.query.filter_by(id_site=data_serialized.get('id_site')).update( + {models.TSite.main_photo: data_serialized.get('id_photo')}) + db.session.commit() + if (data_serialized.get('main_photo')): + del data_serialized['main_photo'] + models.TPhoto.query.filter_by( + id_photo=data_serialized.get('id_photo')).update(data_serialized) db.session.commit() - for image in uploaded_images: - image.save(os.path.join(base_path + image.filename)) + if (image): + for fileName in os.listdir(base_path): + if fileName.endswith(photo_name): + os.remove(base_path + fileName) + image.save(os.path.join(base_path + image.filename)) + else: + for fileName in os.listdir(base_path): + if (fileName != photo_name and fileName.endswith(photo_name)): + os.remove(base_path + fileName) + except Exception as exception: + return (exception), 400 return jsonify('photo added successfully'), 200 - \ No newline at end of file + + +@api.route('/api/deletePhotos', methods=['POST']) +@fnauth.check_auth(6, False, None, None) +def deletePhotos(): + base_path = './static/' + DATA_IMAGES_PATH + photos = request.get_json() + try: + for photo in photos: + photos_query = models.TPhoto.query.filter_by( + id_photo=photo.get('id_photo')).all() + photo_dump = photo_schema.dump(photos_query).data[0] + photo_name = photo_dump.get('path_file_photo') + models.TPhoto.query.filter_by( + id_photo=photo.get('id_photo')).delete() + get_site_by_id = models.TSite.query.filter_by( + id_site=photo_dump.get('t_site')) + site = site_schema.dump(get_site_by_id).data[0] + if (site.get('main_photo') == photo_dump.get('id_photo')): + models.TSite.query.filter_by(id_site=photo_dump.get( + 't_site')).update({models.TSite.main_photo: None}) + db.session.commit() + for fileName in os.listdir(base_path): + if fileName.endswith(photo_name): + os.remove(base_path + fileName) + except Exception as exception: + return (exception), 400 + return jsonify('site has been deleted'), 200 + + +@api.route('/api/communes', methods=['GET']) +def returnAllcommunes(): + try: + get_all_communes = models.Communes.query.order_by('nom_commune').all() + communes = models.CommunesSchema(many=True).dump(get_all_communes).data + except Exception as exception: + return ('error'), 400 + return jsonify(communes), 200 + + +@api.route('/api/logout', methods=['GET']) +def logout(): + resp = Response('', 200) + resp.delete_cookie('token') + return resp diff --git a/backend/app.py b/backend/app.py index 26aadaf8..733c0d65 100755 --- a/backend/app.py +++ b/backend/app.py @@ -4,10 +4,52 @@ from models import (db) import models from flask import Flask +from flask_babel import Babel, gettext, ngettext from flask_cors import CORS +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import text from api import api +import config +import json + +class ReverseProxied(object): + '''Wrap the application in this middleware and configure the + front-end server to add these headers, to let you quietly bind + this to a URL other than / and to an HTTP scheme that is + different than what is used locally. + + In nginx: + location /myprefix { + proxy_pass http://192.168.0.1:5001; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Script-Name /myprefix; + } + + :param app: the WSGI application + ''' + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + script_name = environ.get('HTTP_X_SCRIPT_NAME', '') + if script_name: + environ['SCRIPT_NAME'] = script_name + path_info = environ['PATH_INFO'] + if path_info.startswith(script_name): + environ['PATH_INFO'] = path_info[len(script_name):] + + scheme = environ.get('HTTP_X_SCHEME', '') + if scheme: + environ['wsgi.url_scheme'] = scheme + return self.app(environ, start_response) app = Flask(__name__) +app.config['BABEL_DEFAULT_LOCALE'] = 'fr' +app.config['BABEL_TRANSLATION_DIRECTORIES'] = config.BABEL_TRANSLATION_DIRECTORIES +babel = Babel(app) +#app.wsgi_app = ReverseProxied(app.wsgi_app) CORS(app, supports_credentials=True) app.register_blueprint(main_blueprint) @@ -17,5 +59,21 @@ app.config.from_pyfile('config.py') db.init_app(app) +db = SQLAlchemy() + +@app.context_processor +def inject_dbconf(): + sql = text("SELECT key, value FROM geopaysages.conf") + result = db.engine.execute(sql).fetchall() + rows = [dict(row) for row in result] + conf = {} + for row in rows: + try: + conf[row.get('key')] = json.loads(row.get('value')) + except Exception as exception: + conf[row.get('key')] = row.get('value') + + return dict(dbconf=conf) + if __name__ == "__main__": app.run(debug=True) diff --git a/backend/babel.cfg b/backend/babel.cfg new file mode 100644 index 00000000..0c1feb59 --- /dev/null +++ b/backend/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/tpl/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ \ No newline at end of file diff --git a/backend/config.py.tpl b/backend/config.py.tpl new file mode 100644 index 00000000..d6478452 --- /dev/null +++ b/backend/config.py.tpl @@ -0,0 +1,13 @@ +SQLALCHEMY_DATABASE_URI='postgres://:@:/' + +IGN_KEY='ign_key' +PASS_METHOD='md5' +COOKIE_EXPIRATION = 36000 +COOKIE_AUTORENEW = True +SESSION_TYPE = 'filesystem' +SECRET_KEY = 'secret key' + +# Do not edit except in exceptional cases +DATA_IMAGES_PATH='data/images/'# From ./static dir +DATA_NOTICES_PATH='data/notice-photo/'# From ./static dir +BABEL_TRANSLATION_DIRECTORIES='./i18n'# From ./ dir \ No newline at end of file diff --git a/backend/i18n/fr/LC_MESSAGES/messages.mo b/backend/i18n/fr/LC_MESSAGES/messages.mo new file mode 100644 index 00000000..c986dbb6 Binary files /dev/null and b/backend/i18n/fr/LC_MESSAGES/messages.mo differ diff --git a/backend/i18n/fr/LC_MESSAGES/messages.po b/backend/i18n/fr/LC_MESSAGES/messages.po new file mode 100644 index 00000000..bc1a63c2 --- /dev/null +++ b/backend/i18n/fr/LC_MESSAGES/messages.po @@ -0,0 +1,152 @@ +# French translations for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-12-21 15:08+0100\n" +"PO-Revision-Date: 2018-12-21 11:34+0100\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: routes.py:261 +msgid "map.filter.themes" +msgstr "Thème" + +#: routes.py:265 +msgid "map.filter.subthemes" +msgstr "Sous-thème" + +#: routes.py:269 +msgid "map.filter.township" +msgstr "Commune" + +#: routes.py:273 +msgid "map.filter.years" +msgstr "Année" + +#: tpl/comparator.html:27 +msgid "obs_point.buttons.download" +msgstr "Télécharger" + +#: tpl/comparator.html:102 +msgid "obs_point.buttons.notice" +msgstr "Notice technique pour le photographe" + +#: tpl/comparator.html:105 +msgid "obs_point.buttons.obs" +msgstr "Faire une observation" + +#: tpl/comparator.html:109 +msgid "obs_point.description" +msgstr "Présentation du site" + +#: tpl/comparator.html:116 tpl/comparator.html:129 +msgid "obs_point.buttons.read_more" +msgstr "Lire plus" + +#: tpl/comparator.html:117 tpl/comparator.html:130 +msgid "obs_point.buttons.read_less" +msgstr "Lire moins" + +#: tpl/comparator.html:122 +msgid "obs_point.testimonials" +msgstr "Témoignages" + +#: tpl/gallery.html:3 +msgid "gallery.meta_title" +msgstr "Galerie photo" + +#: tpl/gallery.html:13 +msgid "gallery.title" +msgstr "Galerie photo" + +#: tpl/home.html:3 +msgid "home.meta_title" +msgstr "Observatoire photographique des paysages de Vanoise" + +#: tpl/home.html:16 +msgid "home.title" +msgstr "L'Observatoire photographique des paysages de Vanoise" + +#: tpl/home.html:25 +msgid "home.block.explore_map" +msgstr "Explorer le carte interactive" + +#: tpl/home.html:32 +msgid "home.block.discover" +msgstr "Découvrir ce point d'observation" + +#: tpl/home.html:35 +msgid "home.block.photography" +msgstr "Photographie" + +#: tpl/layout.html:42 +msgid "header.nav.home" +msgstr "Accueil" + +#: tpl/layout.html:45 +msgid "header.nav.gallery" +msgstr "Galerie photo" + +#: tpl/layout.html:48 +msgid "header.nav.map" +msgstr "Carte interactive" + +#: tpl/layout.html:66 +msgid "footer.internal_title" +msgstr "Le site" + +#: tpl/layout.html:70 +msgid "footer.internal_links.home" +msgstr "Accueil" + +#: tpl/layout.html:75 +msgid "footer.internal_links.gallery" +msgstr "Galerie photo" + +#: tpl/layout.html:80 +msgid "footer.internal_links.map" +msgstr "Carte interactive" + +#: tpl/layout.html:85 +msgid "footer.internal_links.contact" +msgstr "Contactez-nous" + +#: tpl/layout.html:92 +msgid "footer.external_title" +msgstr "Le Parc national de Vanoise" + +#: tpl/layout.html:108 +msgid "footer.copyright" +msgstr "© 2018 Parc national de la Vanoise" + +#: tpl/map.html:3 +msgid "map.meta_title" +msgstr "Carte interactive" + +#: tpl/map.html:14 +msgid "map.title" +msgstr "Carte interactive" + +#: tpl/map.html:28 +msgid "map.filters.title" +msgstr "Filtrer par" + +#: tpl/map.html:41 +msgid "map.filters.cancel" +msgstr "annuler les filtres" + +#: tpl/map.html:70 +msgid "map.observation_points.title" +msgstr "Points d'observation" + diff --git a/backend/i18n/messages.pot b/backend/i18n/messages.pot new file mode 100644 index 00000000..0a954302 --- /dev/null +++ b/backend/i18n/messages.pot @@ -0,0 +1,151 @@ +# Translations template for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2018. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2018-12-21 15:08+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.6.0\n" + +#: routes.py:261 +msgid "map.filter.themes" +msgstr "" + +#: routes.py:265 +msgid "map.filter.subthemes" +msgstr "" + +#: routes.py:269 +msgid "map.filter.township" +msgstr "" + +#: routes.py:273 +msgid "map.filter.years" +msgstr "" + +#: tpl/comparator.html:27 +msgid "obs_point.buttons.download" +msgstr "" + +#: tpl/comparator.html:102 +msgid "obs_point.buttons.notice" +msgstr "" + +#: tpl/comparator.html:105 +msgid "obs_point.buttons.obs" +msgstr "" + +#: tpl/comparator.html:109 +msgid "obs_point.description" +msgstr "" + +#: tpl/comparator.html:116 tpl/comparator.html:129 +msgid "obs_point.buttons.read_more" +msgstr "" + +#: tpl/comparator.html:117 tpl/comparator.html:130 +msgid "obs_point.buttons.read_less" +msgstr "" + +#: tpl/comparator.html:122 +msgid "obs_point.testimonials" +msgstr "" + +#: tpl/gallery.html:3 +msgid "gallery.meta_title" +msgstr "" + +#: tpl/gallery.html:13 +msgid "gallery.title" +msgstr "" + +#: tpl/home.html:3 +msgid "home.meta_title" +msgstr "" + +#: tpl/home.html:16 +msgid "home.title" +msgstr "" + +#: tpl/home.html:25 +msgid "home.block.explore_map" +msgstr "" + +#: tpl/home.html:32 +msgid "home.block.discover" +msgstr "" + +#: tpl/home.html:35 +msgid "home.block.photography" +msgstr "" + +#: tpl/layout.html:42 +msgid "header.nav.home" +msgstr "" + +#: tpl/layout.html:45 +msgid "header.nav.gallery" +msgstr "" + +#: tpl/layout.html:48 +msgid "header.nav.map" +msgstr "" + +#: tpl/layout.html:66 +msgid "footer.internal_title" +msgstr "" + +#: tpl/layout.html:70 +msgid "footer.internal_links.home" +msgstr "" + +#: tpl/layout.html:75 +msgid "footer.internal_links.gallery" +msgstr "" + +#: tpl/layout.html:80 +msgid "footer.internal_links.map" +msgstr "" + +#: tpl/layout.html:85 +msgid "footer.internal_links.contact" +msgstr "" + +#: tpl/layout.html:92 +msgid "footer.external_title" +msgstr "" + +#: tpl/layout.html:108 +msgid "footer.copyright" +msgstr "" + +#: tpl/map.html:3 +msgid "map.meta_title" +msgstr "" + +#: tpl/map.html:14 +msgid "map.title" +msgstr "" + +#: tpl/map.html:28 +msgid "map.filters.title" +msgstr "" + +#: tpl/map.html:41 +msgid "map.filters.cancel" +msgstr "" + +#: tpl/map.html:70 +msgid "map.observation_points.title" +msgstr "" + diff --git a/backend/models.py b/backend/models.py index 22e74196..86d5203a 100755 --- a/backend/models.py +++ b/backend/models.py @@ -11,51 +11,68 @@ db = SQLAlchemy() ma = Marshmallow() + class TSite(db.Model): __tablename__ = 't_site' __table_args__ = {'schema': 'geopaysages'} - id_site = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_site = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) name_site = db.Column(db.String) + ref_site = db.Column(db.String) desc_site = db.Column(db.String) + legend_site = db.Column(db.String) testim_site = db.Column(db.String) code_city_site = db.Column(db.String) alti_site = db.Column(db.Integer) - path_file_guide_site = db.Column(db.String(1)) + path_file_guide_site = db.Column(db.String) publish_site = db.Column(db.Boolean) geom = db.Column(Geometry(geometry_type='POINT', srid=4326)) - - + main_photo = db.Column(db.Integer) + + #TODO + #t_ville = db.relationship('Ville', primaryjoin='Ville.ville_code_commune == TSite.code_city_site') class CorSiteSthemeTheme(db.Model): __tablename__ = 'cor_site_stheme_theme' __table_args__ = {'schema': 'geopaysages'} - id_site_stheme_theme = db.Column(db.Integer, nullable=False, server_default=db.FetchedValue()) - id_site = db.Column(db.ForeignKey('geopaysages.t_site.id_site'), primary_key=True, nullable=False) - id_stheme_theme = db.Column(db.ForeignKey('geopaysages.cor_stheme_theme.id_stheme_theme'), primary_key=True, nullable=False) + id_site_stheme_theme = db.Column( + db.Integer, nullable=False, server_default=db.FetchedValue()) + id_site = db.Column(db.ForeignKey( + 'geopaysages.t_site.id_site'), primary_key=True, nullable=False) + id_stheme_theme = db.Column(db.ForeignKey( + 'geopaysages.cor_stheme_theme.id_stheme_theme'), primary_key=True, nullable=False) - t_site = db.relationship('TSite', primaryjoin='CorSiteSthemeTheme.id_site == TSite.id_site', backref='cor_site_stheme_themes') - cor_stheme_theme = db.relationship('CorSthemeTheme', primaryjoin='CorSiteSthemeTheme.id_stheme_theme == CorSthemeTheme.id_stheme_theme', backref='cor_site_stheme_themes') + t_site = db.relationship( + 'TSite', primaryjoin='CorSiteSthemeTheme.id_site == TSite.id_site', backref='cor_site_stheme_themes') + cor_stheme_theme = db.relationship( + 'CorSthemeTheme', primaryjoin='CorSiteSthemeTheme.id_stheme_theme == CorSthemeTheme.id_stheme_theme', backref='cor_site_stheme_themes') class CorSthemeTheme(db.Model): __tablename__ = 'cor_stheme_theme' __table_args__ = {'schema': 'geopaysages'} - id_stheme_theme = db.Column(db.Integer, nullable=False, unique=True, server_default=db.FetchedValue()) - id_stheme = db.Column(db.ForeignKey('geopaysages.dico_stheme.id_stheme'), primary_key=True, nullable=False) - id_theme = db.Column(db.ForeignKey('geopaysages.dico_theme.id_theme'), primary_key=True, nullable=False) + id_stheme_theme = db.Column( + db.Integer, nullable=False, unique=True, server_default=db.FetchedValue()) + id_stheme = db.Column(db.ForeignKey( + 'geopaysages.dico_stheme.id_stheme'), primary_key=True, nullable=False) + id_theme = db.Column(db.ForeignKey( + 'geopaysages.dico_theme.id_theme'), primary_key=True, nullable=False) - dico_stheme = db.relationship('DicoStheme', primaryjoin='CorSthemeTheme.id_stheme == DicoStheme.id_stheme', backref='cor_stheme_themes') - dico_theme = db.relationship('DicoTheme', primaryjoin='CorSthemeTheme.id_theme == DicoTheme.id_theme', backref='cor_stheme_themes') + dico_stheme = db.relationship( + 'DicoStheme', primaryjoin='CorSthemeTheme.id_stheme == DicoStheme.id_stheme', backref='cor_stheme_themes') + dico_theme = db.relationship( + 'DicoTheme', primaryjoin='CorSthemeTheme.id_theme == DicoTheme.id_theme', backref='cor_stheme_themes') class DicoLicencePhoto(db.Model): __tablename__ = 'dico_licence_photo' __table_args__ = {'schema': 'geopaysages'} - id_licence_photo = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_licence_photo = db.Column( + db.Integer, primary_key=True, server_default=db.FetchedValue()) name_licence_photo = db.Column(db.String) description_licence_photo = db.Column(db.String) @@ -64,7 +81,8 @@ class DicoStheme(db.Model): __tablename__ = 'dico_stheme' __table_args__ = {'schema': 'geopaysages'} - id_stheme = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_stheme = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) name_stheme = db.Column(db.String) @@ -72,26 +90,30 @@ class DicoTheme(db.Model): __tablename__ = 'dico_theme' __table_args__ = {'schema': 'geopaysages'} - id_theme = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_theme = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) name_theme = db.Column(db.String) - class TRole(db.Model): __tablename__ = 't_roles' __table_args__ = {'schema': 'utilisateurs'} - groupe = db.Column(db.Boolean, nullable=False, server_default=db.FetchedValue()) - id_role = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + groupe = db.Column(db.Boolean, nullable=False, + server_default=db.FetchedValue()) + id_role = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) identifiant = db.Column(db.String(100)) nom_role = db.Column(db.String(50)) prenom_role = db.Column(db.String(50)) desc_role = db.Column(db.Text) _pass = db.Column('pass', db.String(100)) email = db.Column(db.String(250)) - id_organisme = db.Column(db.ForeignKey('utilisateurs.bib_organismes.id_organisme', onupdate='CASCADE')) + id_organisme = db.Column(db.ForeignKey( + 'utilisateurs.bib_organismes.id_organisme', onupdate='CASCADE')) organisme = db.Column(db.String(32)) - id_unite = db.Column(db.ForeignKey('utilisateurs.bib_unites.id_unite', onupdate='CASCADE')) + id_unite = db.Column(db.ForeignKey( + 'utilisateurs.bib_unites.id_unite', onupdate='CASCADE')) remarques = db.Column(db.Text) pn = db.Column(db.Boolean) session_appli = db.Column(db.String(50)) @@ -99,18 +121,12 @@ class TRole(db.Model): date_update = db.Column(db.DateTime) -class VTest(db.Model): - __tablename__ = 'VTest' - __table_args__ = {'schema': 'geopaysages'} - - id = db.Column(db.Integer,primary_key=True) - class TPhoto(db.Model): __tablename__ = 't_photo' __table_args__ = {'schema': 'geopaysages'} - id_photo = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_photo = db.Column(db.Integer, primary_key=True,server_default=db.FetchedValue()) id_site = db.Column(db.ForeignKey('geopaysages.t_site.id_site')) path_file_photo = db.Column(db.String) id_role = db.Column(db.ForeignKey('utilisateurs.t_roles.id_role')) @@ -118,11 +134,58 @@ class TPhoto(db.Model): filter_date = db.Column(db.Date) legende_photo = db.Column(db.String) display_gal_photo = db.Column(db.Boolean) - id_licence_photo = db.Column(db.ForeignKey('geopaysages.dico_licence_photo.id_licence_photo')) + id_licence_photo = db.Column(db.ForeignKey( + 'geopaysages.dico_licence_photo.id_licence_photo')) + + dico_licence_photo = db.relationship( +'DicoLicencePhoto', primaryjoin='TPhoto.id_licence_photo == DicoLicencePhoto.id_licence_photo', backref='t_photos') + t_role = db.relationship( + 'TRole', primaryjoin='TPhoto.id_role == TRole.id_role', backref='t_photos') + t_site = db.relationship( + 'TSite', primaryjoin='TPhoto.id_site == TSite.id_site', backref='t_photos') + + + +class Communes(db.Model): + __tablename__ = 'communes' + __table_args__ = {'schema': 'geopaysages'} + + code_commune = db.Column(db.String,primary_key=True,server_default=db.FetchedValue()) + nom_commune = db.Column(db.String) + + + +class Ville(db.Model): + __tablename__ = 'villes_france' + __table_args__ = {'schema': 'geopaysages'} + + ville_id = db.Column(db.Integer, primary_key=True,server_default=db.FetchedValue()) + ville_departement = db.Column(db.String) + ville_slug = db.Column(db.String) + ville_nom = db.Column(db.String) + ville_nom_simple = db.Column(db.String) + ville_nom_reel = db.Column(db.String) + ville_nom_soundex = db.Column(db.String) + ville_nom_metaphone = db.Column(db.String) + ville_code_postal = db.Column(db.String) + ville_commune = db.Column(db.String) + ville_code_commune = db.Column(db.String) + ville_arrondissement = db.Column(db.Integer) + ville_canton = db.Column(db.String) + ville_amdi = db.Column(db.Integer) + ville_population_2010 = db.Column(db.Integer) + ville_population_1999 = db.Column(db.Integer) + ville_population_2012 = db.Column(db.Integer) + ville_densite_2010 = db.Column(db.Integer) + ville_surface = db.Column(db.Integer) + ville_longitude_deg = db.Column(db.Integer) + ville_latitude_deg = db.Column(db.Integer) + ville_longitude_grd = db.Column(db.String) + ville_latitude_grd = db.Column(db.String) + ville_latitude_dms = db.Column(db.String) + ville_zmin = db.Column(db.Integer) + ville_zmax = db.Column(db.Integer) - dico_licence_photo = db.relationship('DicoLicencePhoto', primaryjoin='TPhoto.id_licence_photo == DicoLicencePhoto.id_licence_photo', backref='t_photos') - t_role = db.relationship('TRole', primaryjoin='TPhoto.id_role == TRole.id_role', backref='t_photos') - t_site = db.relationship('TSite', primaryjoin='TPhoto.id_site == TSite.id_site', backref='t_photos') class GeographySerializationField(fields.String): def _serialize(self, value, attr, obj): @@ -144,37 +207,62 @@ def _deserialize(self, value, attr, data): return None #schemas# + + class DicoThemeSchema(ma.ModelSchema): class Meta: - fields = ('id_theme','name_theme') + fields = ('id_theme', 'name_theme') + class DicoSthemeSchema(ma.ModelSchema): class Meta: model = DicoStheme + class CorThemeSthemeSchema(ma.ModelSchema): class Meta: fields = ('id_stheme_theme',) + class LicencePhotoSchema(ma.ModelSchema): class Meta: - fields =('id_licence_photo','name_licence_photo','description_licence_photo') - + fields = ('id_licence_photo', 'name_licence_photo','description_licence_photo') + +class RoleSchema(ma.ModelSchema): + class Meta: + fields = ('id_role', 'identifiant', 'nom_role', + 'id_organisme') + class TPhotoSchema(ma.ModelSchema): - dico_licence_photo = ma.Nested(LicencePhotoSchema) + dico_licence_photo = ma.Nested(LicencePhotoSchema) + t_role = ma.Nested(RoleSchema) + class Meta: model = TPhoto - + + class CorSthemeThemeSchema(ma.ModelSchema): - dico_theme = ma.Nested(DicoThemeSchema,only=["id_theme","name_theme"] ) - dico_stheme = ma.Nested(DicoSthemeSchema,only=["id_stheme","name_stheme"] ) + dico_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "name_theme"]) + dico_stheme = ma.Nested(DicoSthemeSchema, only=["id_stheme", "name_stheme"]) + class Meta: fields = ('dico_theme', 'dico_stheme') #model = CorSthemeTheme - + + class TSiteSchema(ma.ModelSchema): geom = GeographySerializationField(attribute='geom') + class Meta: model = TSite - + + +class VilleSchema(ma.ModelSchema): + class Meta: + fields = ('ville_id','ville_code_commune','ville_nom', 'ville_nom_reel') + + +class CommunesSchema(ma.ModelSchema): + class Meta: + model = Communes \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index adcc7186..27321e13 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,6 +3,7 @@ blueprint==3.4.2 cffi==1.11.5 click==6.7 Flask==1.0.2 +Flask-Babel==0.12.2 Flask-Cors==3.0.6 flask-marshmallow==0.9.0 flask-sqlacodegen==1.1.6.1 @@ -22,3 +23,4 @@ https://github.com/PnX-SI/UsersHub-authentification-module/archive/1.2.0.zip six==1.11.0 SQLAlchemy==1.1.13 Werkzeug==0.14.1 +gunicorn==19.9.0 diff --git a/backend/routes.py b/backend/routes.py index a789d329..46834186 100755 --- a/backend/routes.py +++ b/backend/routes.py @@ -1,12 +1,14 @@ -from flask import Flask, render_template, redirect, Blueprint, jsonify, url_for +from flask import Flask, render_template, redirect, Blueprint, jsonify, url_for, abort from flask_sqlalchemy import SQLAlchemy from sqlalchemy import text import models import utils import random from models import (db) -from config import DATA_IMAGES_PATH +from config import DATA_IMAGES_PATH, IGN_KEY import json +from datetime import datetime +from flask_babel import format_datetime, gettext, ngettext main = Blueprint('main', __name__, template_folder='tpl') @@ -17,12 +19,14 @@ photo_schema = models.TPhotoSchema(many=True) site_schema = models.TSiteSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) +villes_schema = models.VilleSchema(many=True) +communes_schema = models.CommunesSchema(many=True) @main.route('/') def home(): - sql = text("SELECT value FROM geopaysages.conf WHERE key = 'home_blocks'") + """ sql = text("SELECT value FROM geopaysages.conf WHERE key = 'home_blocks'") rows = db.engine.execute(sql).fetchall() id_photos = json.loads(rows[0]['value']) get_photos = models.TPhoto.query.filter( @@ -31,9 +35,53 @@ def home(): site_ids = [photo.get('t_site') for photo in dump_pĥotos] get_sites = models.TSite.query.filter(models.TSite.id_site.in_(site_ids)) - dump_sites = site_schema.dump(get_sites).data + dump_sites = site_schema.dump(get_sites).data """ + + sql = text("SELECT * FROM geopaysages.t_site where publish_site=true ORDER BY RANDOM() LIMIT 6") + sites_proxy = db.engine.execute(sql).fetchall() + sites = [dict(row.items()) for row in sites_proxy] + diff_nb = 6 - len(sites) + for x in range(0, diff_nb): + sites.append(sites[x]) + + photo_ids = [] + sites_without_photo = [] + code_communes = [] + for site in sites: + photo_id = site.get('main_photo') + if photo_id: + photo_ids.append(site.get('main_photo')) + else: + sites_without_photo.append(str(site.get('id_site'))) + code_communes.append(site.get('code_city_site')) + + query_photos = models.TPhoto.query.filter( + models.TPhoto.id_photo.in_(photo_ids) + ) + dump_photos = photo_schema.dump(query_photos).data + + if len(sites_without_photo): + sql_missing_photos_str = "select distinct on (id_site) * from geopaysages.t_photo where id_site IN (" + ",".join(sites_without_photo) + ") order by id_site, filter_date desc" + sql_missing_photos = text(sql_missing_photos_str) + missing_photos_result = db.engine.execute(sql_missing_photos).fetchall() + missing_photos = [dict(row) for row in missing_photos_result] + for missing_photo in missing_photos: + missing_photo['t_site'] = missing_photo.get('id_site') + dump_photos.append(missing_photo) + + query_commune = models.Communes.query.filter( + models.Communes.code_commune.in_(code_communes) + ) + dump_communes = communes_schema.dump(query_commune).data + + for site in sites: + id_site = site.get('id_site') + photo = next(photo for photo in dump_photos if (photo.get('t_site') == id_site)) + site['photo'] = utils.getMedium(photo).get('output_url') + site['commune'] = next(commune for commune in dump_communes if (commune.get('code_commune') == site.get('code_city_site'))) + - def get_photo_block(id_photo): + """ def get_photo_block(id_photo): try: photo = next(photo for photo in dump_pĥotos if photo.get( 'id_photo') == id_photo) @@ -51,49 +99,126 @@ def get_photo_block(id_photo): blocks = [ get_photo_block(id_photo) for id_photo in id_photos - ] + ] """ - sites=site_schema.dump(models.TSite.query.all()).data + all_sites=site_schema.dump(models.TSite.query.filter_by(publish_site = True)).data - return render_template('home.html', blocks=blocks, sites=sites) + return render_template('home.html', blocks=sites, sites=all_sites) @main.route('/gallery') def gallery(): - get_photos = models.TPhoto.query.all() - dump_photos = photo_schema.dump(get_photos).data - print(dump_photos) - photos = [{ - 'id_site': photo.get('t_site'), - 'sm': utils.getThumbnail(photo).get('output_url') - } for photo in dump_photos] - - return render_template('gallery.html', photos=photos) + get_sites = models.TSite.query.filter_by(publish_site = True).order_by('name_site') + dump_sites = site_schema.dump(get_sites).data + + #TODO get photos and cities by join on sites query + photo_ids = [] + sites_without_photo = [] + ville_codes = [] + for site in dump_sites: + photo_id = site.get('main_photo') + if photo_id: + photo_ids.append(site.get('main_photo')) + else: + sites_without_photo.append(str(site.get('id_site'))) + ville_codes.append(site.get('code_city_site')) + + query_photos = models.TPhoto.query.filter( + models.TPhoto.id_photo.in_(photo_ids) + ) + dump_photos = photo_schema.dump(query_photos).data + + if len(sites_without_photo): + sql_missing_photos_str = "select distinct on (id_site) * from geopaysages.t_photo where id_site IN (" + ",".join(sites_without_photo) + ") order by id_site, filter_date desc" + sql_missing_photos = text(sql_missing_photos_str) + missing_photos_result = db.engine.execute(sql_missing_photos).fetchall() + missing_photos = [dict(row) for row in missing_photos_result] + for missing_photo in missing_photos: + missing_photo['t_site'] = missing_photo.get('id_site') + dump_photos.append(missing_photo) + + query_villes = models.Communes.query.filter( + models.Communes.code_commune.in_(ville_codes) + ) + dump_villes = communes_schema.dump(query_villes).data + + for site in dump_sites: + id_site = site.get('id_site') + photo = next(photo for photo in dump_photos if (photo.get('t_site') == id_site)) + site['photo'] = utils.getThumbnail(photo).get('output_url') + site['ville'] = next(ville for ville in dump_villes if (ville.get('code_commune') == site.get('code_city_site'))) + + return render_template('gallery.html', sites=dump_sites) @main.route('/comparator/') def comparator(id_site): - get_site_by_id = models.TSite.query.filter_by(id_site = id_site) - site=site_schema.dump(get_site_by_id).data[0] - get_photos_by_site = models.TPhoto.query.filter_by(id_site = id_site) + get_site_by_id = models.TSite.query.filter_by(id_site = id_site, publish_site = True) + site=site_schema.dump(get_site_by_id).data + if len(site) == 0: + return abort(404) + + site = site[0] + get_photos_by_site = models.TPhoto.query.filter_by(id_site = id_site, display_gal_photo=True).order_by('filter_date') photos = photo_schema.dump(get_photos_by_site).data + get_villes = models.Communes.query.filter_by(code_commune = site.get('code_city_site')) + + site['ville'] = communes_schema.dump(get_villes).data[0] def getPhoto(photo): + date_diplay = {} + date_approx = photo.get('date_photo') + filter_date = photo.get('filter_date') + if date_approx: + date_diplay = { + 'md': date_approx, + 'sm': date_approx + } + else: + date_obj = datetime.strptime(filter_date, '%Y-%m-%d') + date_diplay = { + 'md': format_datetime(date_obj, 'yyyy (dd MMMM)'), + 'sm': date_obj.strftime('%Y') + } + captions = [] + licence_photo = photo.get('dico_licence_photo') + if licence_photo: + captions.append(licence_photo.get('name_licence_photo')) + """ author = photo.get('t_role') + if author: + captions.append('%s %s' % ( + photo.get('t_role').get('prenom_role'), + photo.get('t_role').get('nom_role') + )) """ + caption = ' | '.join(captions) + + dl_caption = "%s | %s | réf. : %s | %s" % ( + site.get('name_site'), + site.get('ville').get('nom_commune'), + site.get('ref_site'), + date_diplay.get('md') + ) + + if caption: + dl_caption = '%s | %s' % (dl_caption, caption) + return { 'id': photo.get('id_photo'), - 'sm': url_for('static', filename=DATA_IMAGES_PATH + utils.getThumbnail(photo).get('output_name')), - 'md': url_for('static', filename=DATA_IMAGES_PATH + utils.getMedium(photo).get('output_name')), - 'lg': url_for('static', filename=DATA_IMAGES_PATH + utils.getLarge(photo).get('output_name')), - 'date': photo.get('date_photo') + 'sm': utils.getThumbnail(photo).get('output_url'), + 'md': utils.getMedium(photo).get('output_url'), + 'lg': utils.getLarge(photo, caption).get('output_url'), + 'dl': utils.getDownload(photo, dl_caption).get('output_url'), + 'date': photo.get('filter_date'), + 'date_diplay': date_diplay } photos = [getPhoto(photo) for photo in photos] - return render_template('comparator.html', titre="Bienvenue !", site=site, photos=photos) + return render_template('comparator.html', site=site, photos=photos) @main.route('/map') def map(): - sites=site_schema.dump(models.TSite.query.order_by('name_site').all()).data + sites=site_schema.dump(models.TSite.query.filter_by(publish_site = True).order_by('name_site')).data for site in sites: cor_sthemes_themes = site.get('cor_site_stheme_themes') cor_list = [] @@ -140,19 +265,19 @@ def map(): filters = [{ 'name': 'themes', - 'label': 'Thème', + 'label': gettext(u'map.filter.themes'), 'items': set() }, { 'name': 'subthemes', - 'label': 'Sous-thème', + 'label': gettext(u'map.filter.subthemes'), 'items': set() }, { 'name': 'township', - 'label': 'Commune', + 'label': gettext(u'map.filter.township'), 'items': set() }, { 'name': 'years', - 'label': 'Année', + 'label': gettext(u'map.filter.years'), 'items': set() }] @@ -178,18 +303,23 @@ def map(): subthemes = [{ 'id': item['id_stheme'], - 'label': item['name_stheme'] + 'label': item['name_stheme'], + 'themes': item['themes'] } for item in subthemes] filter_township = [ filter for filter in filters if filter.get('name') == 'township'][0] str_map_in = ["'" + township + "'" for township in filter_township.get('items')] - sql_map_str = "SELECT ville_code_commune AS id, ville_nom_reel AS label FROM geopaysages.villes_france WHERE ville_code_commune IN (" + ",".join( + sql_map_str = "SELECT code_commune AS id, nom_commune AS label FROM geopaysages.communes WHERE code_commune IN (" + ",".join( str_map_in) + ")" sql_map = text(sql_map_str) townships_result = db.engine.execute(sql_map).fetchall() townships = [dict(row) for row in townships_result] + + for site in sites: + site['ville'] = next(township for township in townships if township.get('id') == site.get('township')) + dbs = { 'themes': themes, 'subthemes': subthemes, @@ -205,9 +335,15 @@ def getItem(name, id): 'label': str(year), 'id': year } for year in filter.get('items')] + filter['items'] = sorted(filter['items'], key=lambda k: k['label'], reverse=True) else: filter['items'] = [getItem(filter.get('name'), item_id) for item_id in filter.get('items')] - filter['items'] = sorted(filter['items'], key=lambda k: k['label']) + filter['items'] = sorted(filter['items'], key=lambda k: k['label']) + + return render_template('map.html', filters=filters, sites=sites, ign_Key=IGN_KEY) + - return render_template('map.html', filters=filters, sites=sites) +@main.route('/sample') +def sample(): + return render_template('sample.html') \ No newline at end of file diff --git a/backend/static/assets/images/1px.png b/backend/static/assets/images/1px.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/backend/static/assets/images/1px.png and /dev/null differ diff --git a/backend/static/assets/images/map-world.jpg b/backend/static/assets/images/map-world.jpg deleted file mode 100644 index 0c247a6a..00000000 Binary files a/backend/static/assets/images/map-world.jpg and /dev/null differ diff --git a/backend/static/assets/images/oppv-005-00-2006.jpg b/backend/static/assets/images/oppv-005-00-2006.jpg new file mode 100644 index 00000000..9cead8cf Binary files /dev/null and b/backend/static/assets/images/oppv-005-00-2006.jpg differ diff --git a/backend/static/assets/images/oppv-005-03-2014.jpg b/backend/static/assets/images/oppv-005-03-2014.jpg new file mode 100644 index 00000000..53c0d46f Binary files /dev/null and b/backend/static/assets/images/oppv-005-03-2014.jpg differ diff --git a/backend/static/assets/images/photo1.jpg b/backend/static/assets/images/photo1.jpg deleted file mode 100755 index 74ec9ceb..00000000 Binary files a/backend/static/assets/images/photo1.jpg and /dev/null differ diff --git a/backend/static/assets/images/photo1.png b/backend/static/assets/images/photo1.png deleted file mode 100755 index 74ec9ceb..00000000 Binary files a/backend/static/assets/images/photo1.png and /dev/null differ diff --git a/backend/static/assets/images/photo2.jpg b/backend/static/assets/images/photo2.jpg deleted file mode 100755 index 51250dfa..00000000 Binary files a/backend/static/assets/images/photo2.jpg and /dev/null differ diff --git a/backend/static/css/comparator.css b/backend/static/css/comparator.css index 85beae81..51cb5c53 100644 --- a/backend/static/css/comparator.css +++ b/backend/static/css/comparator.css @@ -2,6 +2,14 @@ position: fixed; } */ +.page-comparator #header { + margin-bottom: 0; +} + +.page-comparator:not(.with-testimonial) .text-collapse.text-collapsable.text-collapsed .target { + max-height: 220px; +} + .page-comparator .page-content { padding: 0 90px; } @@ -45,13 +53,13 @@ cursor: pointer; } -.page-comparator .page-content .compared .img-wrapper .btn-zoom { +.page-comparator .page-content .compared .img-wrapper .img-tools { position: absolute; top: 0; right: 0; } -.page-comparator .page-content .compared .btn-zoom { +.page-comparator .page-content .compared .img-tools .btn { text-shadow: #fff; text-shadow: 1px 1px 0 #FFF; } diff --git a/backend/static/css/gallery.css b/backend/static/css/gallery.css index ccda825b..46c0e1e3 100644 --- a/backend/static/css/gallery.css +++ b/backend/static/css/gallery.css @@ -1,11 +1,12 @@ .page-gallery .container { - min-height: calc(100vh - 472px); + min-height: var(--page-container-min-height); } .page-gallery .picture{ - padding: 20px 10px 0px 10px; + padding: 0 10px 0 10px; overflow: hidden; position: relative; + margin-bottom: 20px; } .page-gallery .picture img{ @@ -29,21 +30,19 @@ right: 10px; padding: 0px 5px; transform: translateY(100%); + line-height: 20px; } -.page-gallery .picture:hover .discover{ - transform: translateY(-100%); +.page-gallery .picture .discover .township { + font-size: .9em; } -@media screen and (max-width: 767px) { - .page-gallery .picture:first-child img{ - filter: brightness(50%); - -webkit-filter: brightness(50%); - -moz-filter: brightness(50%); - -o-filter: brightness(50%); - -ms-filter: brightness(50%); - } - .page-gallery .picture:first-child .discover{ - transform: translateY(-170%); - } +.page-gallery .picture .discover .ref { + position: absolute; + right: 3px; + bottom: -5px; +} + +.page-gallery .picture:hover .discover{ + transform: translateY(-100%); } \ No newline at end of file diff --git a/backend/static/css/home.css b/backend/static/css/home.css index 351f831f..03e4e25a 100644 --- a/backend/static/css/home.css +++ b/backend/static/css/home.css @@ -1,3 +1,7 @@ +.page-home { + overflow-x: hidden; +} + .page-home .page-content { background: url("../images/bg-home.jpg") no-repeat center; background-size: cover; @@ -8,6 +12,7 @@ height: calc(100vh - 160px); width: 50%; margin : 0 auto; + transform-origin: left; } .page-home .block { @@ -37,7 +42,8 @@ top: 0; left: 0; display: flex; - position: absolute + position: absolute; + z-index: 2; } .page-home .legend{ @@ -76,7 +82,7 @@ left: 0; height: 8px; width: 8px; - background-color: var(--color-green); + background-color: var(--color-secondary); transform: translateY(25%); } @@ -89,14 +95,6 @@ font-family: 'Hind', sans-serif; } -.page-home .discover{ - text-align: center; - color: #fff; - font-style: italic; - background-color : rgba(103,233,255,0.5); - z-index: 9999; - transition: transform .5s ease; -} .page-home .discover { width: 100%; display: flex; diff --git a/backend/static/css/map.css b/backend/static/css/map.css index 3144825b..ade9f605 100644 --- a/backend/static/css/map.css +++ b/backend/static/css/map.css @@ -11,7 +11,7 @@ z-index: 10000; height: 100%; color: #FFF; - width: 320px; + width: 270px; transition: transform .3s; } @@ -39,8 +39,8 @@ } .app-map .sidebar .card-group-header-btn { - background: #000; - border: 1px solid #000; + background: rgba(0,0,0,.3); + border: none; font-weight: 700; text-transform: uppercase; } @@ -59,6 +59,11 @@ width: 100%; } +.app-map .sidebar .filters .card-header .btn .icon { + right: auto; + left: -10px; +} + .app-map .sidebar .filters .card { border: none; background: transparent; @@ -74,15 +79,8 @@ border-top: none; } -.app-map .sidebar .filters .card-header .btn.collapsed { - border: 1px solid #FFF; -} - .app-map .sidebar .filters .card-header .btn:not(.collapsed) { - background: #FFF; - border-color: #FFF; - border-bottom-color: var(--color-primary); - color: #000; + color: #FFF; } .app-map .sidebar .filters .card-body { @@ -91,20 +89,37 @@ border: none; } -.app-map .sidebar .filters .card-body .btn { +.app-map .sidebar .filters .card-body .btn, +.app-map .sidebar .btn-site { text-transform: none; white-space: normal; - font-size: 12px; + font-size: 14px; text-align: left; + background: transparent; + color: #FFF; + border-color: transparent; + margin-bottom: 1px; +} +.app-map .sidebar .filters .card-body .btn:hover +{ + background: var(--color-map-filter-bg-hover); + color: var(--color-map-filter-txt-hover); +} +.app-map .sidebar .filters .card-body .btn.active, +.app-map .sidebar .btn-site:hover { + background: var(--color-map-filter-bg-active); + color: var(--color-map-filter-txt-active); } -/* .app-map .sidebar .filters .card:not(:last-child) .card-body .btn { - border-bottom: none; -} */ +.app-map .sidebar .filters .filter-years .btn-group-vertical { + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; +} -.app-map .sidebar .filters .collapse.show .card-body, -.app-map .sidebar .filters .collapsing .card-body { - border-bottom: 1px solid #FFF; +.app-map .sidebar .filters .filter-years .card-body .btn { + display: inline-block; + width: 30%; } .app-map .sidebar .card-group { @@ -116,16 +131,6 @@ border-bottom: 1px solid transparent; } -.app-map .sidebar .btn-site { - text-align: left; - text-transform: none; - white-space: normal; -} - -.app-map .sidebar .btn-site:hover { - background: #111; -} - .app-map .leaflet-popup-content { text-align: center; margin: 0; @@ -133,13 +138,17 @@ .app-map .leaflet-popup-content-wrapper { border-radius: 0; + background: rgba(0, 0, 0, .8); + color: #FFF; + box-shadow: none; } .app-map .leaflet-popup-content .title { width: 150px; text-align: left; padding: 3px; - font-size: 14px; + padding-left: 5px; + font-size: 12px; } .app-map .leaflet-popup-content .img { diff --git a/backend/static/css/page-sample.css b/backend/static/css/page-sample.css new file mode 100644 index 00000000..d1489338 --- /dev/null +++ b/backend/static/css/page-sample.css @@ -0,0 +1,3 @@ +.page-sample .container { + min-height: var(--page-container-min-height); +} \ No newline at end of file diff --git a/backend/static/css/style.css b/backend/static/css/style.css index 62f5b318..8d56d216 100644 --- a/backend/static/css/style.css +++ b/backend/static/css/style.css @@ -1,10 +1,17 @@ :root { --color-primary: #00188F; --color-primary-light: #303f8a; - --color-blue : #283371; - --color-green : #7AB929; - --color-secondary: #6db61e; + --color-secondary: #78BE20; --color-secondary-light: #72d409; + + --color-discover: rgba(153, 214, 234, 0.5); + + --color-map-filter-txt-hover: #FFF; + --color-map-filter-bg-hover: #99d6ea61; + --color-map-filter-txt-active: #000; + --color-map-filter-bg-active: #99D6EA; + + --page-container-min-height: calc(100vh - 415px); } .swiper-pagination-bullet-active { @@ -51,10 +58,16 @@ button{ border-bottom: solid 1px #000; } +.no-underline { + text-decoration: none !important; +} + .btn { - text-transform: uppercase; border-radius: 0; } +.btn:not(.btn-no-uppercase) { + text-transform: uppercase; +} .btn:focus { box-shadow: none !important; } @@ -67,6 +80,11 @@ button{ border-color: var(--color-primary); } +.btn-primary:hover { + background-color: var(--color-primary-light); + border-color: var(--color-primary-light); +} + .btn-secondary { background-color: var(--color-secondary); border-color: var(--color-secondary); @@ -112,6 +130,16 @@ button{ border-color: var(--color-primary); } +.btn-clear-primary { + color: #FFF; + background-color: transparent; + border-color: transparent; +} + +.btn-clear-primary:hover { + background-color: var(--color-primary); +} + .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active, .show>.btn-outline-primary.dropdown-toggle { @@ -135,26 +163,6 @@ button{ font-size: 1.5em; } -.blue-btn { - background-color: var(--color-blue); - color: #fff; - border: none; - padding: 5px 30px; - text-transform: uppercase; - font-size: 13px; - width: fit-content; -} - -.green-btn{ - background-color: var(--color-green); - color: #fff; - border: none; - padding: 5px 30px; - text-transform: uppercase; - font-size: 13px; - width: fit-content; -} - .progress-bar { background-color: var(--color-primary); } @@ -163,7 +171,7 @@ button{ text-align: center; color: #fff; font-style: italic; - background-color : rgba(103,233,255,0.5); + background-color : var(--color-discover); z-index: 9999; transition: transform .5s ease; } @@ -181,6 +189,13 @@ button{ /* End of bootstrap */ +/* Leaflet */ + +.leaflet-container .easy-button-container .icon { + font-size: 1.5em; + vertical-align: sub; +} + body, html { font-family: 'Crimson Text', serif; } @@ -202,6 +217,7 @@ h1 .light, h2 .light, h3 .light, h4 .light, h5 .light, h6 .light { padding: 40px 60px 10px 60px; margin-top: 20px; position: relative; + height: 260px; } .app-footer hr { @@ -254,7 +270,10 @@ h1 .light, h2 .light, h3 .light, h4 .light, h5 .light, h6 .light { .header-title { justify-content: center; + margin-left: 0; margin-bottom: -8px; + padding-left: 120px; + padding-right: 120px; } .header-logo { @@ -317,6 +336,10 @@ h1 .light, h2 .light, h3 .light, h4 .light, h5 .light, h6 .light { background: transparent; } +.modal-fs .modal-content { + overflow: hidden; +} + .modal-fs .modal-dialog { color: #FFF; margin: 0; @@ -337,7 +360,7 @@ h1 .light, h2 .light, h3 .light, h4 .light, h5 .light, h6 .light { .modal-fs .modal-header { border: none; - padding-bottom: 0; + padding: 0; } .modal-fs .modal-header .close, @@ -350,15 +373,13 @@ h1 .light, h2 .light, h3 .light, h4 .light, h5 .light, h6 .light { } .modal-fs.modal-img .modal-body { - display: flex; - padding-top: 0; + padding: 0; + height: calc(100% - 44px); } -.modal-fs.modal-img .img-wrapper { - flex: 1; - display: flex; - align-items: center; - justify-content: center; +.modal-fs.modal-img figure { + height: 100%; + margin: 0; } .modal-fs.modal-img .img-wrapper:not(:first-child) { @@ -369,18 +390,38 @@ h1 .light, h2 .light, h3 .light, h4 .light, h5 .light, h6 .light { padding-right: 5px; } -.modal-fs.modal-img .img-wrapper img { - max-width: 100%; - max-height: 100%; - width: auto; +.modal-fs.modal-img figure img { + max-width: calc(100% - 60px); + max-height: calc(100% - 80px); height: auto; + width: auto; + position: absolute; + left: 50%; + top: 50%; + margin-top: -35px; + transform: translate(-50%, -50%); +} + +.modal-fs.modal-img figcaption { + font-family: 'Hind', sans-serif; + font-weight: 700 !important; + position: absolute; + bottom: 10px; + width: 100%; + margin: 0; + text-align: center; } /* HEADER */ -header h1{ +header h1 { text-transform: uppercase; font-size: 1.5em; + line-height: 20px; +} + +header h3 { + line-height: 20px; } .navbar-collapse { @@ -405,11 +446,12 @@ footer div:first-child{ display: flex; flex-wrap: wrap; justify-content: space-between; - width: 80%; } footer img{ - height: fit-content; + align-self: center; + height: 110px; + width: auto; } footer ul{ diff --git a/backend/static/custom/css/custom-style.css b/backend/static/custom/css/custom-style.css new file mode 100644 index 00000000..1f4bd9b6 --- /dev/null +++ b/backend/static/custom/css/custom-style.css @@ -0,0 +1,27 @@ +/* css clors */ +:root { + --color-primary: #00188F; + --color-primary-light: #0069d9; + --color-secondary: #78BE20; + --color-secondary-light: #72d409; + + --color-discover: rgba(153, 214, 234, 0.5); + + --color-map-filter-txt-hover: #FFF; + --color-map-filter-bg-hover: #99d6ea61; + --color-map-filter-txt-active: #000; + --color-map-filter-bg-active: #99D6EA; + + --page-container-min-height: calc(100vh - 415px); +} + +/* css Footer */ +footer{ + color: #fff; +} +.app-footer { + background: #666; +} +.app-footer hr { + border-color: #FFF; +} diff --git a/backend/static/custom/logo/logo_txt_blanc.png b/backend/static/custom/logo/logo_txt_blanc.png new file mode 100644 index 00000000..a658e034 Binary files /dev/null and b/backend/static/custom/logo/logo_txt_blanc.png differ diff --git a/backend/static/custom/logo/logo_txt_color.png b/backend/static/custom/logo/logo_txt_color.png new file mode 100644 index 00000000..e91287cc Binary files /dev/null and b/backend/static/custom/logo/logo_txt_color.png differ diff --git a/backend/static/images/sample.png b/backend/static/images/sample.png new file mode 100644 index 00000000..950c8cc0 Binary files /dev/null and b/backend/static/images/sample.png differ diff --git a/backend/static/js/comparator.js b/backend/static/js/comparator.js index 8d287adc..fdfd8468 100644 --- a/backend/static/js/comparator.js +++ b/backend/static/js/comparator.js @@ -6,6 +6,7 @@ oppv.comparator = (options) => { return { pinned: -1, nextComparedIndex: 0, + comparedPhotoIndexes: [0, 1], comparedPhotos: [options.photos[0], options.photos[1]], zoomPhotos: [], textCollapses: ['description', 'testimonial'], @@ -43,9 +44,11 @@ oppv.comparator = (options) => { this.textCollapsables = [] this.textCollapses.forEach(name => { let el = this.$refs['text_collapse_' + name] + if (!el) + return; let target = el.getElementsByClassName('target')[0] if (target.scrollHeight > target.clientHeight) - this.textCollapsables.push(name) + this.textCollapsables.push(name) }) this.textCollapseds = collapseds }) @@ -77,17 +80,46 @@ oppv.comparator = (options) => { }); }, initMap() { + let layersConf = _.get(options.dbconf, 'map_layers', []); + if (!Array.isArray(layersConf)) { + layersConf = []; + } + if (!layersConf.length) { + layersConf.push({ + "label": "OSM classic", + "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + "options": { + "maxZoom": 18, + "attribution": "© OpenStreetMap" + } + }) + } + let mapLayers = layersConf.map(layer => { + return { + label: layer.label, + layer: L.tileLayer(layer.url, layer.options) + } + }) + map = L.map(this.$refs.map, { center: options.site.geom, - zoom: 8 + zoom: options.dbconf.zoom_map_comparator, + layers: [mapLayers[0].layer] }) - const tileLayer = L.tileLayer( - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: '© OpenStreetMap', - } - ) - tileLayer.addTo(map) + + L.control.scale({ + position: 'bottomleft', + imperial: false + }).addTo(map); + + if (mapLayers.length > 1) { + const controlLayers = {}; + mapLayers.forEach(layerConf => { + controlLayers[layerConf.label] = layerConf.layer + }) + L.control.layers(controlLayers).addTo(map); + } + L.marker(options.site.geom).addTo(map) }, isPinned(i) { @@ -116,12 +148,32 @@ oppv.comparator = (options) => { const photo = options.photos[i] let comparedIndex = this.nextComparedIndex - if (this.pinned > -1) + if (this.pinned > -1) { comparedIndex = this.getUnpinned() - else //increment nextComparedIndex and back to 0 if > comparedPhotos.length - this.nextComparedIndex = ++this.nextComparedIndex % this.comparedPhotos.length + this.$set(this.comparedPhotos, comparedIndex, Object.assign({}, photo)) + } else { + /* //increment nextComparedIndex and back to 0 if > comparedPhotos.length + this.nextComparedIndex = ++this.nextComparedIndex % this.comparedPhotos.length */ + this.comparedPhotoIndexes.push(i) + this.comparedPhotoIndexes.shift() + + let comparedPhotos = this.comparedPhotoIndexes.map(index => { + let photo = options.photos[index] + return Object.assign({}, photo) + }) + + comparedPhotos.sort((a, b) => { + return a.date < b.date ? -1 : 1 + }) - this.$set(this.comparedPhotos, comparedIndex, Object.assign({}, photo)) + this.comparedPhotos = comparedPhotos.map(comparedPhoto => { + const oldComparedPhoto = this.comparedPhotos.find(oldComparedPhoto => { + return oldComparedPhoto.date == comparedPhoto.date + }) + comparedPhoto.comparedLoaded = Boolean(oldComparedPhoto) + return comparedPhoto; + }) + } }, onComparedLoaded(i) { this.$set(this.comparedPhotos, i, Object.assign(this.comparedPhotos[i], { @@ -133,6 +185,9 @@ oppv.comparator = (options) => { this.zoomPhotos = this.comparedPhotos else this.zoomPhotos = [this.comparedPhotos[i]] + }, + onDownloadClick(photo) { + window.saveAs(photo.dl, photo.dl.split('/').pop()); } } }) diff --git a/backend/static/js/home.js b/backend/static/js/home.js index 27ed7526..cadeb9f0 100644 --- a/backend/static/js/home.js +++ b/backend/static/js/home.js @@ -2,6 +2,7 @@ var oppv = oppv || {}; oppv.initHome = (options) => { let gutter = 20; + let pageContainer = document.querySelector('.page-content'); let container = document.querySelector('.blocks'); let block_map = document.querySelector('.block-map'); @@ -50,6 +51,7 @@ oppv.initHome = (options) => { }) function onResize() { + container.style.transform = null; if (window.matchMedia("(min-width: 800px)").matches) { container.style.height = null; @@ -58,8 +60,10 @@ oppv.initHome = (options) => { let containerW = container.clientWidth; let containerH = container.clientHeight; + let blockSM = (containerH * 1 / 3); let blockBG = (containerH * 2 / 3); + let nextContainerW = (blockSM * 5 + gutter * 3); block_1.style.height = (blockSM * 2) + 'px'; block_1.style.width = block_1.style.height; @@ -92,7 +96,17 @@ oppv.initHome = (options) => { block_6.style.left = (blockBG * 2 + gutter * 3) + 'px'; block_6.style.top = (blockSM * 2 + gutter) + 'px'; - container.style.width = (blockSM * 5 + gutter * 3) + 'px'; + container.style.width = nextContainerW + 'px'; + + /** + * transform: scale(0.66); + transform-origin: left; + */ + + if (nextContainerW > pageContainer.clientWidth) { + let rate = pageContainer.clientWidth / nextContainerW; + container.style.transform = 'scale(' + rate + ')' + } } else { diff --git a/backend/static/js/map.js b/backend/static/js/map.js index aac6df26..10674fa1 100644 --- a/backend/static/js/map.js +++ b/backend/static/js/map.js @@ -1,15 +1,21 @@ var oppv = oppv || {}; oppv.initMap = (options) => { + const storedSelectedFilters = JSON.parse(localStorage.getItem('oppv.map.selectedFilters')); options.filters.forEach(filter => { + filter.selectedItems = []; filter.items.forEach(item => { + let isSelected = _.get(storedSelectedFilters, filter.name, []).indexOf(item.id) > -1 Object.assign(item, { - isSelected: false + isSelected: isSelected }) + if (isSelected) + filter.selectedItems.push(item); }) }) const filters = _.cloneDeep(options.filters) let map; let markers = [] + let mapBounds; new Vue({ el: '#js-app-map', @@ -17,7 +23,8 @@ oppv.initMap = (options) => { return { isSidebarCollapsed: false, filters: filters, - sites: options.sites + sites: options.sites, + selectedSites: [] } }, mounted() { @@ -25,23 +32,79 @@ oppv.initMap = (options) => { }, methods: { initMap() { + let layersConf = _.get(options.dbconf, 'map_layers', []); + if (!Array.isArray(layersConf)) { + layersConf = []; + } + if (!layersConf.length) { + layersConf.push({ + "label": "OSM classic", + "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + "options": { + "maxZoom": 18, + "attribution": "© OpenStreetMap" + } + }) + } + let mapLayers = layersConf.map(layer => { + return { + label: layer.label, + layer: L.tileLayer(layer.url, layer.options) + } + }) + map = L.map(this.$refs.map, { - zoomControl: false + zoomControl: false, + layers: [mapLayers[0].layer] }) - const tileLayer = L.tileLayer( - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: '© OpenStreetMap', - } - ) + + L.control.scale({ + position: 'bottomright', + imperial: false + }).addTo(map); + L.control.zoom({ position: 'topright' }).addTo(map); - tileLayer.addTo(map) + + L.easyButton({ + position: 'topright', + states: [{ + stateName: 'reset', + onClick: function (button, map) { + if (mapBounds) + map.fitBounds(mapBounds.bbox, mapBounds.options) + }, + icon: 'icon ion-md-locate' + }] + }).addTo(map) + + map.addControl(new L.Control.Fullscreen({ + position: 'topright' + })); + + if (mapLayers.length > 1) { + const controlLayers = {}; + mapLayers.forEach(layerConf => { + controlLayers[layerConf.label] = layerConf.layer + }) + L.control.layers(controlLayers).addTo(map); + } + + this.setFilters() + }, + onCancelClick() { + filters.forEach(filter => { + filter.items.forEach(item => { + item.isSelected = false; + }); + filter.items.selectedItems = []; + }); + this.updateFilters() this.setFilters() }, onFilterClick(filter, item) { - //this.updateFilters() + this.updateFilters() this.setFilters() }, updateFilters() { @@ -67,25 +130,36 @@ oppv.initMap = (options) => { }).items = selectedSubthemes }, setFilters() { + let storedSelectedFilters = {} let selectedFilters = filters.map(filter => { let selectedItems = filter.items.filter(item => { return item.isSelected }) - let items = selectedItems.length ? selectedItems : filter.items + //let items = selectedItems.length ? selectedItems : filter.items + let selectedIds = selectedItems.map(item => { + return item.id + }) + if (selectedIds.length) + storedSelectedFilters[filter.name] = selectedIds return Object.assign({}, filter, { - ids: items.map(item => { - return item.id - }) + selectedIds: selectedIds }) }) + localStorage.setItem('oppv.map.selectedFilters', JSON.stringify(storedSelectedFilters)) let selectedSites = [] options.sites.forEach(site => { + site.marker = null unmatchedProp = selectedFilters.find(filter => { let prop = _.get(site, filter.name) if (!Array.isArray(prop)) prop = [prop] - return !_.intersection(prop, filter.ids).length + let ids = filter.selectedIds.length ? + filter.selectedIds : + filter.items.map(item => { + return item.id + }) + return !_.intersection(prop, ids).length }) if (!unmatchedProp) selectedSites.push(site) @@ -102,7 +176,9 @@ oppv.initMap = (options) => { lats.push(site.latlon[0]) lons.push(site.latlon[1]) let marker = L.marker(site.latlon) - marker.bindPopup('
' + site.name_site + '
', { + site.marker = marker + markerText = site.name_site + '
' + site.ville.label + marker.bindPopup('
' + markerText + '
', { closeButton: false }) marker.on('mouseover', (e) => { @@ -112,21 +188,34 @@ oppv.initMap = (options) => { marker.closePopup() }) marker.on('click', (e) => { + //TODO window.location.href = site.link.replace('http://127.0.0.1:8000', '') }) marker.addTo(map) markers.push(marker) }); + this.selectedSites = selectedSites; if (!markers.length) return lats.sort() lons.sort() - map.fitBounds([ - [lats[0], lons[0]], - [lats[lats.length - 1], lons[lons.length - 1]] - ], { - maxZoom: 11 - }) + mapBounds = { + bbox: [ + [lats[0], lons[0]], + [lats[lats.length - 1], lons[lons.length - 1]] + ], + options: { + maxZoom: options.dbconf.zoom_max_fitbounds_map + } + } + map.fitBounds(mapBounds.bbox, mapBounds.options) + }, + onSiteMousover(site) { + site.marker.openPopup() + map.panTo(site.latlon) + }, + onSiteMouseout(site) { + site.marker.closePopup() } } }) diff --git a/backend/static/vendor/FileSaver.js b/backend/static/vendor/FileSaver.js new file mode 100644 index 00000000..c1dfd17d --- /dev/null +++ b/backend/static/vendor/FileSaver.js @@ -0,0 +1,180 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof exports !== "undefined") { + factory(); + } else { + var mod = { + exports: {} + }; + factory(); + global.FileSaver = mod.exports; + } + })(this, function () { + "use strict"; + + /* + * FileSaver.js + * A saveAs() FileSaver implementation. + * + * By Eli Grey, http://eligrey.com + * + * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) + * source : http://purl.eligrey.com/github/FileSaver.js + */ + // The one and only way of getting global scope in all environments + // https://stackoverflow.com/q/3277182/1008999 + var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0; + + function bom(blob, opts) { + if (typeof opts === 'undefined') opts = { + autoBom: false + };else if (typeof opts !== 'object') { + console.warn('Depricated: Expected third argument to be a object'); + opts = { + autoBom: !opts + }; + } // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { + type: blob.type + }); + } + + return blob; + } + + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + + xhr.onerror = function () { + console.error('could not download file'); + }; + + xhr.send(); + } + + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker + + xhr.open('HEAD', url, false); + xhr.send(); + return xhr.status >= 200 && xhr.status <= 299; + } // `a.click()` doesn't work for all browsers (#465) + + + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); + } catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } + + var saveAs = _global.saveAs || // probably in some web worker + typeof window !== 'object' || window !== _global ? function saveAs() {} + /* noop */ + // Use download attribute first if possible (#193 Lumia mobile) + : 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; + var a = document.createElement('a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + + if (a.origin !== location.origin) { + corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); + } else { + click(a); + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 4E4); // 40s + + setTimeout(function () { + click(a); + }, 0); + } + } // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { + click(a); + }); + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only avalible on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + + if (popup) { + popup.document.title = popup.document.body.innerText = 'downloading...'; + } + + if (typeof blob === 'string') return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + + if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') { + // Safari doesn't allow downloading of blob urls + var reader = new FileReader(); + + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) popup.location.href = url;else location = url; + popup = null; // reverse-tabnabbing #460 + }; + + reader.readAsDataURL(blob); + } else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) popup.location = url;else location.href = url; + popup = null; // reverse-tabnabbing #460 + + setTimeout(function () { + URL.revokeObjectURL(url); + }, 4E4); // 40s + } + }; + _global.saveAs = saveAs.saveAs = saveAs; + + if (typeof module !== 'undefined') { + module.exports = saveAs; + } + }); \ No newline at end of file diff --git a/backend/static/vendor/leaflet-easybutton/easy-button.css b/backend/static/vendor/leaflet-easybutton/easy-button.css new file mode 100644 index 00000000..18ce9ac1 --- /dev/null +++ b/backend/static/vendor/leaflet-easybutton/easy-button.css @@ -0,0 +1,56 @@ +.leaflet-bar button, +.leaflet-bar button:hover { + background-color: #fff; + border: none; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; +} + +.leaflet-bar button { + background-position: 50% 50%; + background-repeat: no-repeat; + overflow: hidden; + display: block; +} + +.leaflet-bar button:hover { + background-color: #f4f4f4; +} + +.leaflet-bar button:first-of-type { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-bar button:last-of-type { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; +} + +.leaflet-bar.disabled, +.leaflet-bar button.disabled { + cursor: default; + pointer-events: none; + opacity: .4; +} + +.easy-button-button .button-state{ + display: block; + width: 100%; + height: 100%; + position: relative; +} + + +.leaflet-touch .leaflet-bar button { + width: 30px; + height: 30px; + line-height: 30px; +} diff --git a/backend/static/vendor/leaflet-easybutton/easy-button.js b/backend/static/vendor/leaflet-easybutton/easy-button.js new file mode 100644 index 00000000..fbd300f6 --- /dev/null +++ b/backend/static/vendor/leaflet-easybutton/easy-button.js @@ -0,0 +1,371 @@ +(function(){ + +// This is for grouping buttons into a bar +// takes an array of `L.easyButton`s and +// then the usual `.addTo(map)` +L.Control.EasyBar = L.Control.extend({ + + options: { + position: 'topleft', // part of leaflet's defaults + id: null, // an id to tag the Bar with + leafletClasses: true // use leaflet classes? + }, + + + initialize: function(buttons, options){ + + if(options){ + L.Util.setOptions( this, options ); + } + + this._buildContainer(); + this._buttons = []; + + for(var i = 0; i < buttons.length; i++){ + buttons[i]._bar = this; + buttons[i]._container = buttons[i].button; + this._buttons.push(buttons[i]); + this.container.appendChild(buttons[i].button); + } + + }, + + + _buildContainer: function(){ + this._container = this.container = L.DomUtil.create('div', ''); + this.options.leafletClasses && L.DomUtil.addClass(this.container, 'leaflet-bar easy-button-container leaflet-control'); + this.options.id && (this.container.id = this.options.id); + }, + + + enable: function(){ + L.DomUtil.addClass(this.container, 'enabled'); + L.DomUtil.removeClass(this.container, 'disabled'); + this.container.setAttribute('aria-hidden', 'false'); + return this; + }, + + + disable: function(){ + L.DomUtil.addClass(this.container, 'disabled'); + L.DomUtil.removeClass(this.container, 'enabled'); + this.container.setAttribute('aria-hidden', 'true'); + return this; + }, + + + onAdd: function () { + return this.container; + }, + + addTo: function (map) { + this._map = map; + + for(var i = 0; i < this._buttons.length; i++){ + this._buttons[i]._map = map; + } + + var container = this._container = this.onAdd(map), + pos = this.getPosition(), + corner = map._controlCorners[pos]; + + L.DomUtil.addClass(container, 'leaflet-control'); + + if (pos.indexOf('bottom') !== -1) { + corner.insertBefore(container, corner.firstChild); + } else { + corner.appendChild(container); + } + + return this; + } + +}); + +L.easyBar = function(){ + var args = [L.Control.EasyBar]; + for(var i = 0; i < arguments.length; i++){ + args.push( arguments[i] ); + } + return new (Function.prototype.bind.apply(L.Control.EasyBar, args)); +}; + +// L.EasyButton is the actual buttons +// can be called without being grouped into a bar +L.Control.EasyButton = L.Control.extend({ + + options: { + position: 'topleft', // part of leaflet's defaults + + id: null, // an id to tag the button with + + type: 'replace', // [(replace|animate)] + // replace swaps out elements + // animate changes classes with all elements inserted + + states: [], // state names look like this + // { + // stateName: 'untracked', + // onClick: function(){ handle_nav_manually(); }; + // title: 'click to make inactive', + // icon: 'fa-circle', // wrapped with + // } + + leafletClasses: true, // use leaflet styles for the button + tagName: 'button', + }, + + + + initialize: function(icon, onClick, title, id){ + + // clear the states manually + this.options.states = []; + + // add id to options + if(id != null){ + this.options.id = id; + } + + // storage between state functions + this.storage = {}; + + // is the last item an object? + if( typeof arguments[arguments.length-1] === 'object' ){ + + // if so, it should be the options + L.Util.setOptions( this, arguments[arguments.length-1] ); + } + + // if there aren't any states in options + // use the early params + if( this.options.states.length === 0 && + typeof icon === 'string' && + typeof onClick === 'function'){ + + // turn the options object into a state + this.options.states.push({ + icon: icon, + onClick: onClick, + title: typeof title === 'string' ? title : '' + }); + } + + // curate and move user's states into + // the _states for internal use + this._states = []; + + for(var i = 0; i < this.options.states.length; i++){ + this._states.push( new State(this.options.states[i], this) ); + } + + this._buildButton(); + + this._activateState(this._states[0]); + + }, + + _buildButton: function(){ + + this.button = L.DomUtil.create(this.options.tagName, ''); + + if (this.options.tagName === 'button') { + this.button.setAttribute('type', 'button'); + } + + if (this.options.id ){ + this.button.id = this.options.id; + } + + if (this.options.leafletClasses){ + L.DomUtil.addClass(this.button, 'easy-button-button leaflet-bar-part leaflet-interactive'); + } + + // don't let double clicks and mousedown get to the map + L.DomEvent.addListener(this.button, 'dblclick', L.DomEvent.stop); + L.DomEvent.addListener(this.button, 'mousedown', L.DomEvent.stop); + L.DomEvent.addListener(this.button, 'mouseup', L.DomEvent.stop); + + // take care of normal clicks + L.DomEvent.addListener(this.button,'click', function(e){ + L.DomEvent.stop(e); + this._currentState.onClick(this, this._map ? this._map : null ); + this._map && this._map.getContainer().focus(); + }, this); + + // prep the contents of the control + if(this.options.type == 'replace'){ + this.button.appendChild(this._currentState.icon); + } else { + for(var i=0;i"']/) ){ + + // if so, the user should have put in html + // so move forward as such + tmpIcon = ambiguousIconString; + + // then it wasn't html, so + // it's a class list, figure out what kind + } else { + ambiguousIconString = ambiguousIconString.replace(/(^\s*|\s*$)/g,''); + tmpIcon = L.DomUtil.create('span', ''); + + if( ambiguousIconString.indexOf('fa-') === 0 ){ + L.DomUtil.addClass(tmpIcon, 'fa ' + ambiguousIconString) + } else if ( ambiguousIconString.indexOf('glyphicon-') === 0 ) { + L.DomUtil.addClass(tmpIcon, 'glyphicon ' + ambiguousIconString) + } else { + L.DomUtil.addClass(tmpIcon, /*rollwithit*/ ambiguousIconString) + } + + // make this a string so that it's easy to set innerHTML below + tmpIcon = tmpIcon.outerHTML; + } + + return tmpIcon; +} + +})(); diff --git a/backend/static/vendor/leaflet-fullscreen/Leaflet.fullscreen.min.js b/backend/static/vendor/leaflet-fullscreen/Leaflet.fullscreen.min.js new file mode 100644 index 00000000..184cc7fa --- /dev/null +++ b/backend/static/vendor/leaflet-fullscreen/Leaflet.fullscreen.min.js @@ -0,0 +1 @@ +L.Control.Fullscreen=L.Control.extend({options:{position:"topleft",title:{"false":"View Fullscreen","true":"Exit Fullscreen"}},onAdd:function(map){var container=L.DomUtil.create("div","leaflet-control-fullscreen leaflet-bar leaflet-control");this.link=L.DomUtil.create("a","leaflet-control-fullscreen-button leaflet-bar-part",container);this.link.href="#";this._map=map;this._map.on("fullscreenchange",this._toggleTitle,this);this._toggleTitle();L.DomEvent.on(this.link,"click",this._click,this);return container},_click:function(e){L.DomEvent.stopPropagation(e);L.DomEvent.preventDefault(e);this._map.toggleFullscreen(this.options)},_toggleTitle:function(){this.link.title=this.options.title[this._map.isFullscreen()]}});L.Map.include({isFullscreen:function(){return this._isFullscreen||false},toggleFullscreen:function(options){var container=this.getContainer();if(this.isFullscreen()){if(options&&options.pseudoFullscreen){this._disablePseudoFullscreen(container)}else if(document.exitFullscreen){document.exitFullscreen()}else if(document.mozCancelFullScreen){document.mozCancelFullScreen()}else if(document.webkitCancelFullScreen){document.webkitCancelFullScreen()}else if(document.msExitFullscreen){document.msExitFullscreen()}else{this._disablePseudoFullscreen(container)}}else{if(options&&options.pseudoFullscreen){this._enablePseudoFullscreen(container)}else if(container.requestFullscreen){container.requestFullscreen()}else if(container.mozRequestFullScreen){container.mozRequestFullScreen()}else if(container.webkitRequestFullscreen){container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}else if(container.msRequestFullscreen){container.msRequestFullscreen()}else{this._enablePseudoFullscreen(container)}}},_enablePseudoFullscreen:function(container){L.DomUtil.addClass(container,"leaflet-pseudo-fullscreen");this._setFullscreen(true);this.invalidateSize();this.fire("fullscreenchange")},_disablePseudoFullscreen:function(container){L.DomUtil.removeClass(container,"leaflet-pseudo-fullscreen");this._setFullscreen(false);this.invalidateSize();this.fire("fullscreenchange")},_setFullscreen:function(fullscreen){this._isFullscreen=fullscreen;var container=this.getContainer();if(fullscreen){L.DomUtil.addClass(container,"leaflet-fullscreen-on")}else{L.DomUtil.removeClass(container,"leaflet-fullscreen-on")}},_onFullscreenChange:function(e){var fullscreenElement=document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.msFullscreenElement;if(fullscreenElement===this.getContainer()&&!this._isFullscreen){this._setFullscreen(true);this.fire("fullscreenchange")}else if(fullscreenElement!==this.getContainer()&&this._isFullscreen){this._setFullscreen(false);this.fire("fullscreenchange")}}});L.Map.mergeOptions({fullscreenControl:false});L.Map.addInitHook(function(){if(this.options.fullscreenControl){this.fullscreenControl=new L.Control.Fullscreen(this.options.fullscreenControl);this.addControl(this.fullscreenControl)}var fullscreenchange;if("onfullscreenchange"in document){fullscreenchange="fullscreenchange"}else if("onmozfullscreenchange"in document){fullscreenchange="mozfullscreenchange"}else if("onwebkitfullscreenchange"in document){fullscreenchange="webkitfullscreenchange"}else if("onmsfullscreenchange"in document){fullscreenchange="MSFullscreenChange"}if(fullscreenchange){var onFullscreenChange=L.bind(this._onFullscreenChange,this);this.whenReady(function(){L.DomEvent.on(document,fullscreenchange,onFullscreenChange)});this.on("unload",function(){L.DomEvent.off(document,fullscreenchange,onFullscreenChange)})}});L.control.fullscreen=function(options){return new L.Control.Fullscreen(options)}; \ No newline at end of file diff --git a/backend/static/vendor/leaflet-fullscreen/fullscreen.png b/backend/static/vendor/leaflet-fullscreen/fullscreen.png new file mode 100644 index 00000000..7384960a Binary files /dev/null and b/backend/static/vendor/leaflet-fullscreen/fullscreen.png differ diff --git a/backend/static/vendor/leaflet-fullscreen/fullscreen@2x.png b/backend/static/vendor/leaflet-fullscreen/fullscreen@2x.png new file mode 100644 index 00000000..9fca7f87 Binary files /dev/null and b/backend/static/vendor/leaflet-fullscreen/fullscreen@2x.png differ diff --git a/backend/static/vendor/leaflet-fullscreen/leaflet.fullscreen.css b/backend/static/vendor/leaflet-fullscreen/leaflet.fullscreen.css new file mode 100644 index 00000000..f4892578 --- /dev/null +++ b/backend/static/vendor/leaflet-fullscreen/leaflet.fullscreen.css @@ -0,0 +1,40 @@ +.leaflet-control-fullscreen a { + background:#fff url(fullscreen.png) no-repeat 0 0; + background-size:26px 52px; + } + .leaflet-touch .leaflet-control-fullscreen a { + background-position: 2px 2px; + } + .leaflet-fullscreen-on .leaflet-control-fullscreen a { + background-position:0 -26px; + } + .leaflet-touch.leaflet-fullscreen-on .leaflet-control-fullscreen a { + background-position: 2px -24px; + } + +/* Do not combine these two rules; IE will break. */ +.leaflet-container:-webkit-full-screen { + width:100%!important; + height:100%!important; + } +.leaflet-container.leaflet-fullscreen-on { + width:100%!important; + height:100%!important; + } + +.leaflet-pseudo-fullscreen { + position:fixed!important; + width:100%!important; + height:100%!important; + top:0!important; + left:0!important; + z-index:99999; + } + +@media + (-webkit-min-device-pixel-ratio:2), + (min-resolution:192dpi) { + .leaflet-control-fullscreen a { + background-image:url(fullscreen@2x.png); + } + } diff --git a/backend/static/vendor/vue.js b/backend/static/vendor/vue.js new file mode 100644 index 00000000..4ef7ff1b --- /dev/null +++ b/backend/static/vendor/vue.js @@ -0,0 +1,11944 @@ +/*! + * Vue.js v2.6.10 + * (c) 2014-2019 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Vue = factory()); +}(this, function () { 'use strict'; + + /* */ + + var emptyObject = Object.freeze({}); + + // These helpers produce better VM code in JS engines due to their + // explicitness and function inlining. + function isUndef (v) { + return v === undefined || v === null + } + + function isDef (v) { + return v !== undefined && v !== null + } + + function isTrue (v) { + return v === true + } + + function isFalse (v) { + return v === false + } + + /** + * Check if value is primitive. + */ + function isPrimitive (value) { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) + } + + /** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + */ + function isObject (obj) { + return obj !== null && typeof obj === 'object' + } + + /** + * Get the raw type string of a value, e.g., [object Object]. + */ + var _toString = Object.prototype.toString; + + function toRawType (value) { + return _toString.call(value).slice(8, -1) + } + + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ + function isPlainObject (obj) { + return _toString.call(obj) === '[object Object]' + } + + function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' + } + + /** + * Check if val is a valid array index. + */ + function isValidArrayIndex (val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val) + } + + function isPromise (val) { + return ( + isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function' + ) + } + + /** + * Convert a value to a string that is actually rendered. + */ + function toString (val) { + return val == null + ? '' + : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) + ? JSON.stringify(val, null, 2) + : String(val) + } + + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n + } + + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap ( + str, + expectsLowerCase + ) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } + } + + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + /** + * Remove an item from an array. + */ + function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } + } + + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) + } + + /** + * Create a cached version of a pure function. + */ + function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) + } + + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) + }); + + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + }); + + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() + }); + + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + + /* istanbul ignore next */ + function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn + } + + function nativeBind (fn, ctx) { + return fn.bind(ctx) + } + + var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + + /** + * Convert an Array-like object to a real Array. + */ + function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret + } + + /** + * Mix properties into target object. + */ + function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to + } + + /** + * Merge an Array of Objects into a single Object. + */ + function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res + } + + /* eslint-disable no-unused-vars */ + + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop (a, b, c) {} + + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + + /* eslint-enable no-unused-vars */ + + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } + } + + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 + } + + /** + * Ensure a function is called only once. + */ + function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } + } + + var SSR_ATTR = 'data-server-rendered'; + + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch' + ]; + + /* */ + + + + var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }); + + /* */ + + /** + * unicode letters used for parsing html tags, component names and property paths. + * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + * skipping \u10000-\uEFFFF due to it freezing up PhantomJS + */ + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + + /** + * Check if a string starts with $ or _ + */ + function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F + } + + /** + * Define a property. + */ + function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Parse simple path. + */ + var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); + function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } + } + + /* */ + + // can we use __proto__? + var hasProto = '__proto__' in {}; + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); + var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + var isPhantomJS = UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + + // Firefox has a "watch" function on Object.prototype... + var nativeWatch = ({}).watch; + + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} + } + + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer + }; + + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + + /* istanbul ignore next */ + function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) + } + + var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + + var _Set; + /* istanbul ignore if */ // $flow-disable-line + if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /*@__PURE__*/(function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); + } + + /* */ + + var warn = noop; + var tip = noop; + var generateComponentTrace = (noop); // work around flow check + var formatComponentName = (noop); + + { + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; + } + + /* */ + + var uid = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ + var Dep = function Dep () { + this.id = uid++; + this.subs = []; + }; + + Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); + }; + + Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + + Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + + function pushTarget (target) { + targetStack.push(target); + Dep.target = target; + } + + function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* */ + + var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory + ) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; + + var prototypeAccessors = { child: { configurable: true } }; + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + prototypeAccessors.child.get = function () { + return this.componentInstance + }; + + Object.defineProperties( VNode.prototype, prototypeAccessors ); + + var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node + }; + + function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) + } + + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); + }); + + /* */ + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + + function toggleObserving (value) { + shouldObserve = value; + } + + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + // helpers + + /** + * Augment a target Object or Array by intercepting + * the prototype chain using __proto__ + */ + function protoAugment (target, src) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ + } + + /** + * Augment a target Object or Array by defining + * hidden properties. + */ + /* istanbul ignore next */ + function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob + } + + /** + * Define a reactive property on an Object. + */ + function defineReactive$$1 ( + obj, + key, + val, + customSetter, + shallow + ) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if (customSetter) { + customSetter(); + } + // #7981: for accessor properties without setter + if (getter && !setter) { return } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + + /** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ + function set (target, key, val) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val + } + + /** + * Delete a property and trigger change if necessary. + */ + function del (target, key) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); + } + + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + + /* */ + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ + var strats = config.optionMergeStrategies; + + /** + * Options with restrictions + */ + { + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; + } + + /** + * Helper that recursively merges two data objects together. + */ + function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + + var keys = hasSymbol + ? Reflect.ownKeys(from) + : Object.keys(from); + + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + // in case the object is already observed... + if (key === '__ob__') { continue } + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if ( + toVal !== fromVal && + isPlainObject(toVal) && + isPlainObject(fromVal) + ) { + mergeData(toVal, fromVal); + } + } + return to + } + + /** + * Data + */ + function mergeDataOrFn ( + parentVal, + childVal, + vm + ) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } + } + + strats.data = function ( + parentVal, + childVal, + vm + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) + }; + + /** + * Hooks and props are merged as arrays. + */ + function mergeHook ( + parentVal, + childVal + ) { + var res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal; + return res + ? dedupeHooks(res) + : res + } + + function dedupeHooks (hooks) { + var res = []; + for (var i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]); + } + } + return res + } + + LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; + }); + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + function mergeAssets ( + parentVal, + childVal, + vm, + key + ) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } + } + + ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + strats.watch = function ( + parentVal, + childVal, + vm, + key + ) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret + }; + + /** + * Other object hashes. + */ + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + parentVal, + childVal, + vm, + key + ) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret + }; + strats.provide = mergeDataOrFn; + + /** + * Default strategy. + */ + var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal + }; + + /** + * Validate component names + */ + function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } + } + + function validateComponentName (name) { + if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'should conform to valid custom element name in html5 specification.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ + function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; + } + + /** + * Normalize all injections into Object-based format + */ + function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + + /** + * Normalize raw function directives into object format. + */ + function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def$$1 = dirs[key]; + if (typeof def$$1 === 'function') { + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + + function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ + function mergeOptions ( + parent, + child, + vm + ) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + + // Apply extends and mixins on the child options, + // but only if it is a raw options object that isn't + // the result of another mergeOptions call. + // Only merged options has the _base property. + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ + function resolveAsset ( + options, + type, + id, + warnMissing + ) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res + } + + /* */ + + + + function validateProp ( + key, + propOptions, + propsData, + vm + ) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value + } + + /** + * Get the default value of a prop. + */ + function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def + } + + /** + * Assert whether a prop is valid. + */ + function assertProp ( + prop, + name, + value, + vm, + absent + ) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + + if (!valid) { + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + + function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } + } + + /** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ + function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' + } + + function isSameType (a, b) { + return getType(a) === getType(b) + } + + function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 + } + + function getInvalidTypeMessage (name, value, expectedTypes) { + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + // check if we need to specify expected value + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + // check if we need to specify received value + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message + } + + function styleValue (value, type) { + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + return ("" + (Number(value))) + } else { + return ("" + value) + } + } + + function isExplicable (value) { + var explicitTypes = ['string', 'number', 'boolean']; + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + + function isBoolean () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + + /* */ + + function handleError (err, vm, info) { + // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. + // See: https://github.com/vuejs/vuex/issues/1505 + pushTarget(); + try { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); + } finally { + popTarget(); + } + } + + function invokeWithErrorHandling ( + handler, + context, + args, + vm, + info + ) { + var res; + try { + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); + // issue #9511 + // avoid catch triggering multiple times when nested calls + res._handled = true; + } + } catch (e) { + handleError(e, vm, info); + } + return res + } + + function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + // if the user intentionally throws the original error in the handler, + // do not log it twice + if (e !== err) { + logError(e, null, 'config.errorHandler'); + } + } + } + logError(err, vm, info); + } + + function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } + } + + /* */ + + var isUsingMicroTask = false; + + var callbacks = []; + var pending = false; + + function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + + // Here we have async deferring wrappers using microtasks. + // In 2.5 we used (macro) tasks (in combination with microtasks). + // However, it has subtle problems when state is changed right before repaint + // (e.g. #6813, out-in transitions). + // Also, using (macro) tasks in event handler would cause some weird behaviors + // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). + // So we now use microtasks everywhere, again. + // A major drawback of this tradeoff is that there are some scenarios + // where microtasks have too high a priority and fire in between supposedly + // sequential events (e.g. #4521, #6690, which have workarounds) + // or even between bubbling of the same event (#6566). + var timerFunc; + + // The nextTick behavior leverages the microtask queue, which can be accessed + // via either native Promise.then or MutationObserver. + // MutationObserver has wider support, however it is seriously bugged in + // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It + // completely stops working after triggering a few times... so, if native + // Promise is available, we will use it: + /* istanbul ignore next, $flow-disable-line */ + if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + timerFunc = function () { + p.then(flushCallbacks); + // In problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== 'undefined' && ( + isNative(MutationObserver) || + // PhantomJS and iOS 7.x + MutationObserver.toString() === '[object MutationObserverConstructor]' + )) { + // Use MutationObserver where native Promise is not available, + // e.g. PhantomJS, iOS7, Android 4.4 + // (#6466 MutationObserver is unreliable in IE11) + var counter = 1; + var observer = new MutationObserver(flushCallbacks); + var textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + isUsingMicroTask = true; + } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + // Fallback to setImmediate. + // Techinically it leverages the (macro) task queue, + // but it is still a better choice than setTimeout. + timerFunc = function () { + setImmediate(flushCallbacks); + }; + } else { + // Fallback to setTimeout. + timerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + + function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + timerFunc(); + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } + } + + /* */ + + var mark; + var measure; + + { + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + + /* not type checking this file because flow doesn't play well with Proxy */ + + var initProxy; + + { + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var warnReservedPrefix = function (target, key) { + warn( + "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals' + + 'See: https://vuejs.org/v2/api/#data', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || + (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + + /* */ + + var seenObjects = new _Set(); + + /** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ + function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + + function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } + } + + /* */ + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } + }); + + function createFnInvoker (fns, vm) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + // return handler return value for single handlers + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler") + } + } + invoker.fns = fns; + return invoker + } + + function updateListeners ( + on, + oldOn, + add, + remove$$1, + createOnceHandler, + vm + ) { + var name, def$$1, cur, old, event; + for (name in on) { + def$$1 = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } + } + + /* */ + + function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; + } + + /* */ + + function extractPropsFromVNodeData ( + data, + Ctor, + tag + ) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res + } + + function checkProp ( + res, + hash, + key, + altKey, + preserve + ) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false + } + + /* */ + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children + } + + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.