diff --git a/.eslintrc.json b/.eslintrc.json index 9d32b1e7e2..08069c84bb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,8 @@ "skel": true, "setupPokemonMarker": true, + "isTouchDevice": true, + "pokemonSprites": true, "noLabelsStyle": true, "darkStyle": true, "light2Style": true, diff --git a/Gruntfile.js b/Gruntfile.js index 87c372c1c3..dd1532c432 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -28,6 +28,7 @@ module.exports = function(grunt) { files: { 'static/dist/js/app.built.js': 'static/js/app.js', 'static/dist/js/map.built.js': 'static/js/map.js', + 'static/dist/js/map.common.built.js': 'static/js/map.common.js', 'static/dist/js/mobile.built.js': 'static/js/mobile.js', 'static/dist/js/stats.built.js': 'static/js/stats.js', 'static/dist/js/statistics.built.js': 'static/js/statistics.js', @@ -47,6 +48,7 @@ module.exports = function(grunt) { files: { 'static/dist/js/app.min.js': 'static/dist/js/app.built.js', 'static/dist/js/map.min.js': 'static/dist/js/map.built.js', + 'static/dist/js/map.common.min.js': 'static/dist/js/map.common.built.js', 'static/dist/js/mobile.min.js': 'static/dist/js/mobile.built.js', 'static/dist/js/stats.min.js': 'static/dist/js/stats.built.js', 'static/dist/js/statistics.min.js': 'static/dist/js/statistics.built.js', diff --git a/pogom/app.py b/pogom/app.py index e2525c6429..6ac0f3e9a9 100755 --- a/pogom/app.py +++ b/pogom/app.py @@ -113,8 +113,7 @@ def raw_data(self): d['seen'] = Pokemon.get_seen(selected_duration) if request.args.get('appearances', 'false') == 'true': - d['appearances'] = Pokemon.get_appearances(request.args.get('pokemonid'), - request.args.get('last', type=float), selected_duration) + d['appearances'] = Pokemon.get_appearances(request.args.get('pokemonid'), selected_duration) if request.args.get('appearancesDetails', 'false') == 'true': d['appearancesTimes'] = Pokemon.get_appearances_times_by_spawnpoint(request.args.get('pokemonid'), diff --git a/pogom/models.py b/pogom/models.py index e64efc3cbb..2e43bb5650 100755 --- a/pogom/models.py +++ b/pogom/models.py @@ -16,6 +16,8 @@ from playhouse.migrate import migrate, MySQLMigrator, SqliteMigrator from datetime import datetime, timedelta from base64 import b64encode +from cachetools import TTLCache +from cachetools import cached from . import config from .utils import get_pokemon_name, get_pokemon_rarity, get_pokemon_types, get_args @@ -26,6 +28,7 @@ args = get_args() flaskDb = FlaskDB() +cache = TTLCache(maxsize=100, ttl=60 * 5) db_schema_version = 7 @@ -157,6 +160,7 @@ def get_active_by_id(ids, swLat, swLng, neLat, neLng): return pokemons @classmethod + @cached(cache) def get_seen(cls, timediff): if timediff: timediff = datetime.utcnow() - timediff @@ -197,23 +201,21 @@ def get_seen(cls, timediff): return {'pokemon': pokemons, 'total': total} @classmethod - def get_appearances(cls, pokemon_id, last_appearance, timediff): + def get_appearances(cls, pokemon_id, timediff): ''' :param pokemon_id: id of pokemon that we need appearances for - :param last_appearance: time of last appearance of pokemon after which we are getting appearances :param timediff: limiting period of the selection :return: list of pokemon appearances over a selected period ''' if timediff: timediff = datetime.utcnow() - timediff query = (Pokemon - .select(Pokemon.latitude, Pokemon.longitude, Pokemon.pokemon_id, fn.Count(Pokemon.spawnpoint_id).alias('count'), Pokemon.spawnpoint_id, Pokemon.disappear_time) + .select(Pokemon.latitude, Pokemon.longitude, Pokemon.pokemon_id, fn.Count(Pokemon.spawnpoint_id).alias('count'), Pokemon.spawnpoint_id) .where((Pokemon.pokemon_id == pokemon_id) & - (Pokemon.disappear_time > datetime.utcfromtimestamp(last_appearance / 1000.0)) & (Pokemon.disappear_time > timediff) ) .order_by(Pokemon.disappear_time.asc()) - .group_by(Pokemon.spawnpoint_id) + .group_by(Pokemon.latitude, Pokemon.longitude, Pokemon.pokemon_id, Pokemon.spawnpoint_id) .dicts() ) diff --git a/requirements.txt b/requirements.txt index bea267f3d5..7c9f8d0e73 100755 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,5 @@ requests==2.10.0 PySocks==1.5.6 git+https://github.com/maddhatter/Flask-CacheBust.git@38d940cc4f18b5fcb5687746294e0360640a107e#egg=flask_cachebust protobuf_to_dict==0.1.0 +cachetools==1.1.6 + diff --git a/static/css/statistics.css b/static/css/statistics.css index 74d20b7095..afabf06402 100644 --- a/static/css/statistics.css +++ b/static/css/statistics.css @@ -73,7 +73,7 @@ table td{ } .container .item .name{ - height: 30px; + height: 36px; line-height: 30px; } diff --git a/static/js/map.common.js b/static/js/map.common.js new file mode 100644 index 0000000000..932cac3067 --- /dev/null +++ b/static/js/map.common.js @@ -0,0 +1,852 @@ +/*eslint no-unused-vars: "off"*/ + +var noLabelsStyle = [{ + featureType: 'poi', + elementType: 'labels', + stylers: [{ + visibility: 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}] +var light2Style = [{ + 'elementType': 'geometry', + 'stylers': [{ + 'hue': '#ff4400' + }, { + 'saturation': -68 + }, { + 'lightness': -4 + }, { + 'gamma': 0.72 + }] +}, { + 'featureType': 'road', + 'elementType': 'labels.icon' +}, { + 'featureType': 'landscape.man_made', + 'elementType': 'geometry', + 'stylers': [{ + 'hue': '#0077ff' + }, { + 'gamma': 3.1 + }] +}, { + 'featureType': 'water', + 'stylers': [{ + 'hue': '#00ccff' + }, { + 'gamma': 0.44 + }, { + 'saturation': -33 + }] +}, { + 'featureType': 'poi.park', + 'stylers': [{ + 'hue': '#44ff00' + }, { + 'saturation': -23 + }] +}, { + 'featureType': 'water', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'hue': '#007fff' + }, { + 'gamma': 0.77 + }, { + 'saturation': 65 + }, { + 'lightness': 99 + }] +}, { + 'featureType': 'water', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'gamma': 0.11 + }, { + 'weight': 5.6 + }, { + 'saturation': 99 + }, { + 'hue': '#0091ff' + }, { + 'lightness': -86 + }] +}, { + 'featureType': 'transit.line', + 'elementType': 'geometry', + 'stylers': [{ + 'lightness': -48 + }, { + 'hue': '#ff5e00' + }, { + 'gamma': 1.2 + }, { + 'saturation': -23 + }] +}, { + 'featureType': 'transit', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'saturation': -64 + }, { + 'hue': '#ff9100' + }, { + 'lightness': 16 + }, { + 'gamma': 0.47 + }, { + 'weight': 2.7 + }] +}] +var darkStyle = [{ + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'saturation': 36 + }, { + 'color': '#b39964' + }, { + 'lightness': 40 + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'visibility': 'on' + }, { + 'color': '#000000' + }, { + 'lightness': 16 + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'administrative', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 20 + }] +}, { + 'featureType': 'administrative', + 'elementType': 'geometry.stroke', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 17 + }, { + 'weight': 1.2 + }] +}, { + 'featureType': 'landscape', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 20 + }] +}, { + 'featureType': 'poi', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 21 + }] +}, { + 'featureType': 'road.highway', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 17 + }] +}, { + 'featureType': 'road.highway', + 'elementType': 'geometry.stroke', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 29 + }, { + 'weight': 0.2 + }] +}, { + 'featureType': 'road.arterial', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 18 + }] +}, { + 'featureType': 'road.local', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#181818' + }, { + 'lightness': 16 + }] +}, { + 'featureType': 'transit', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 19 + }] +}, { + 'featureType': 'water', + 'elementType': 'geometry', + 'stylers': [{ + 'lightness': 17 + }, { + 'color': '#525252' + }] +}] +var pGoStyle = [{ + 'featureType': 'landscape.man_made', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#a1f199' + }] +}, { + 'featureType': 'landscape.natural.landcover', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#37bda2' + }] +}, { + 'featureType': 'landscape.natural.terrain', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#37bda2' + }] +}, { + 'featureType': 'poi.attraction', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'visibility': 'on' + }] +}, { + 'featureType': 'poi.business', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#e4dfd9' + }] +}, { + 'featureType': 'poi.business', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'poi.park', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#37bda2' + }] +}, { + 'featureType': 'road', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#84b09e' + }] +}, { + 'featureType': 'road', + 'elementType': 'geometry.stroke', + 'stylers': [{ + 'color': '#fafeb8' + }, { + 'weight': '1.25' + }] +}, { + 'featureType': 'road.highway', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'water', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#5ddad6' + }] +}] +var light2StyleNoLabels = [{ + 'elementType': 'geometry', + 'stylers': [{ + 'hue': '#ff4400' + }, { + 'saturation': -68 + }, { + 'lightness': -4 + }, { + 'gamma': 0.72 + }] +}, { + 'featureType': 'road', + 'elementType': 'labels.icon' +}, { + 'featureType': 'landscape.man_made', + 'elementType': 'geometry', + 'stylers': [{ + 'hue': '#0077ff' + }, { + 'gamma': 3.1 + }] +}, { + 'featureType': 'water', + 'stylers': [{ + 'hue': '#00ccff' + }, { + 'gamma': 0.44 + }, { + 'saturation': -33 + }] +}, { + 'featureType': 'poi.park', + 'stylers': [{ + 'hue': '#44ff00' + }, { + 'saturation': -23 + }] +}, { + 'featureType': 'water', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'hue': '#007fff' + }, { + 'gamma': 0.77 + }, { + 'saturation': 65 + }, { + 'lightness': 99 + }] +}, { + 'featureType': 'water', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'gamma': 0.11 + }, { + 'weight': 5.6 + }, { + 'saturation': 99 + }, { + 'hue': '#0091ff' + }, { + 'lightness': -86 + }] +}, { + 'featureType': 'transit.line', + 'elementType': 'geometry', + 'stylers': [{ + 'lightness': -48 + }, { + 'hue': '#ff5e00' + }, { + 'gamma': 1.2 + }, { + 'saturation': -23 + }] +}, { + 'featureType': 'transit', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'saturation': -64 + }, { + 'hue': '#ff9100' + }, { + 'lightness': 16 + }, { + 'gamma': 0.47 + }, { + 'weight': 2.7 + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}] +var darkStyleNoLabels = [{ + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'administrative', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 20 + }] +}, { + 'featureType': 'administrative', + 'elementType': 'geometry.stroke', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 17 + }, { + 'weight': 1.2 + }] +}, { + 'featureType': 'landscape', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 20 + }] +}, { + 'featureType': 'poi', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 21 + }] +}, { + 'featureType': 'road.highway', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 17 + }] +}, { + 'featureType': 'road.highway', + 'elementType': 'geometry.stroke', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 29 + }, { + 'weight': 0.2 + }] +}, { + 'featureType': 'road.arterial', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 18 + }] +}, { + 'featureType': 'road.local', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#181818' + }, { + 'lightness': 16 + }] +}, { + 'featureType': 'transit', + 'elementType': 'geometry', + 'stylers': [{ + 'color': '#000000' + }, { + 'lightness': 19 + }] +}, { + 'featureType': 'water', + 'elementType': 'geometry', + 'stylers': [{ + 'lightness': 17 + }, { + 'color': '#525252' + }] +}] +var pGoStyleNoLabels = [{ + 'featureType': 'landscape.man_made', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#a1f199' + }] +}, { + 'featureType': 'landscape.natural.landcover', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#37bda2' + }] +}, { + 'featureType': 'landscape.natural.terrain', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#37bda2' + }] +}, { + 'featureType': 'poi.attraction', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'visibility': 'on' + }] +}, { + 'featureType': 'poi.business', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#e4dfd9' + }] +}, { + 'featureType': 'poi.business', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'poi.park', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#37bda2' + }] +}, { + 'featureType': 'road', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#84b09e' + }] +}, { + 'featureType': 'road', + 'elementType': 'geometry.stroke', + 'stylers': [{ + 'color': '#fafeb8' + }, { + 'weight': '1.25' + }] +}, { + 'featureType': 'road.highway', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'water', + 'elementType': 'geometry.fill', + 'stylers': [{ + 'color': '#5ddad6' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [{ + 'visibility': 'off' + }] +}, { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [{ + 'visibility': 'off' + }] +}] +var pokemonSprites = { + normal: { + columns: 12, + iconWidth: 30, + iconHeight: 30, + spriteWidth: 360, + spriteHeight: 390, + filename: 'static/icons-sprite.png', + name: 'Normal' + }, + highres: { + columns: 7, + iconWidth: 65, + iconHeight: 65, + spriteWidth: 455, + spriteHeight: 1430, + filename: 'static/icons-large-sprite.png', + name: 'High-Res' + }, + shuffle: { + columns: 7, + iconWidth: 65, + iconHeight: 65, + spriteWidth: 455, + spriteHeight: 1430, + filename: 'static/icons-shuffle-sprite.png', + name: 'Shuffle' + } +} + +// +// LocalStorage helpers +// + +var StoreTypes = { + Boolean: { + parse: function (str) { + switch (str.toLowerCase()) { + case '1': + case 'true': + case 'yes': + return true + default: + return false + } + }, + stringify: function (b) { + return b ? 'true' : 'false' + } + }, + JSON: { + parse: function (str) { + return JSON.parse(str) + }, + stringify: function (json) { + return JSON.stringify(json) + } + }, + String: { + parse: function (str) { + return str + }, + stringify: function (str) { + return str + } + }, + Number: { + parse: function (str) { + return parseInt(str, 10) + }, + stringify: function (number) { + return number.toString() + } + } +} + +var StoreOptions = { + 'map_style': { + default: 'roadmap', + type: StoreTypes.String + }, + 'remember_select_exclude': { + default: [], + type: StoreTypes.JSON + }, + 'remember_select_notify': { + default: [], + type: StoreTypes.JSON + }, + 'remember_select_rarity_notify': { + default: [], + type: StoreTypes.JSON + }, + 'showGyms': { + default: false, + type: StoreTypes.Boolean + }, + 'showPokemon': { + default: true, + type: StoreTypes.Boolean + }, + 'showPokestops': { + default: true, + type: StoreTypes.Boolean + }, + 'showLuredPokestopsOnly': { + default: 0, + type: StoreTypes.Number + }, + 'showScanned': { + default: false, + type: StoreTypes.Boolean + }, + 'showSpawnpoints': { + default: false, + type: StoreTypes.Boolean + }, + 'showRanges': { + default: false, + type: StoreTypes.Boolean + }, + 'playSound': { + default: false, + type: StoreTypes.Boolean + }, + 'geoLocate': { + default: false, + type: StoreTypes.Boolean + }, + 'lockMarker': { + default: isTouchDevice(), // default to true if touch device + type: StoreTypes.Boolean + }, + 'startAtUserLocation': { + default: false, + type: StoreTypes.Boolean + }, + 'followMyLocation': { + default: false, + type: StoreTypes.Boolean + }, + 'followMyLocationPosition': { + default: [], + type: StoreTypes.JSON + }, + 'pokemonIcons': { + default: 'highres', + type: StoreTypes.String + }, + 'iconSizeModifier': { + default: 0, + type: StoreTypes.Number + }, + 'searchMarkerStyle': { + default: 'google', + type: StoreTypes.String + }, + 'locationMarkerStyle': { + default: 'none', + type: StoreTypes.String + }, + 'zoomLevel': { + default: 16, + type: StoreTypes.Number + } +} + +var Store = { + getOption: function (key) { + var option = StoreOptions[key] + if (!option) { + throw new Error('Store key was not defined ' + key) + } + return option + }, + get: function (key) { + var option = this.getOption(key) + var optionType = option.type + var rawValue = localStorage[key] + if (rawValue === null || rawValue === undefined) { + return option.default + } + var value = optionType.parse(rawValue) + return value + }, + set: function (key, value) { + var option = this.getOption(key) + var optionType = option.type || StoreTypes.String + var rawValue = optionType.stringify(value) + localStorage[key] = rawValue + }, + reset: function (key) { + localStorage.removeItem(key) + } +} + +var mapData = { + pokemons: {}, + gyms: {}, + pokestops: {}, + lurePokemons: {}, + scanned: {}, + spawnpoints: {} +} + +function getGoogleSprite (index, sprite, displayHeight) { + displayHeight = Math.max(displayHeight, 3) + var scale = displayHeight / sprite.iconHeight + // Crop icon just a tiny bit to avoid bleedover from neighbor + var scaledIconSize = new google.maps.Size(scale * sprite.iconWidth - 1, scale * sprite.iconHeight - 1) + var scaledIconOffset = new google.maps.Point( + (index % sprite.columns) * sprite.iconWidth * scale + 0.5, + Math.floor(index / sprite.columns) * sprite.iconHeight * scale + 0.5) + var scaledSpriteSize = new google.maps.Size(scale * sprite.spriteWidth, scale * sprite.spriteHeight) + var scaledIconCenterOffset = new google.maps.Point(scale * sprite.iconWidth / 2, scale * sprite.iconHeight / 2) + + return { + url: sprite.filename, + size: scaledIconSize, + scaledSize: scaledSpriteSize, + origin: scaledIconOffset, + anchor: scaledIconCenterOffset + } +} + +function setupPokemonMarker (item, map, isBounceDisabled) { + // Scale icon size up with the map exponentially + var iconSize = 2 + (map.getZoom() - 3) * (map.getZoom() - 3) * 0.2 + Store.get('iconSizeModifier') + var pokemonIndex = item['pokemon_id'] - 1 + var sprite = pokemonSprites[Store.get('pokemonIcons')] || pokemonSprites['highres'] + var icon = getGoogleSprite(pokemonIndex, sprite, iconSize) + + var animationDisabled = false + if (isBounceDisabled === true) { + animationDisabled = true + } + + var marker = new google.maps.Marker({ + position: { + lat: item['latitude'], + lng: item['longitude'] + }, + zIndex: 9999, + map: map, + icon: icon, + animationDisabled: animationDisabled + }) + + return marker +} + +function isTouchDevice () { + // Should cover most browsers + return 'ontouchstart' in window || navigator.maxTouchPoints +} diff --git a/static/js/map.js b/static/js/map.js index 04c554e563..5b76c50377 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -32,809 +32,12 @@ var searchMarker var storeZoom = true var scanPath -var noLabelsStyle = [{ - featureType: 'poi', - elementType: 'labels', - stylers: [{ - visibility: 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}] -var light2Style = [{ - 'elementType': 'geometry', - 'stylers': [{ - 'hue': '#ff4400' - }, { - 'saturation': -68 - }, { - 'lightness': -4 - }, { - 'gamma': 0.72 - }] -}, { - 'featureType': 'road', - 'elementType': 'labels.icon' -}, { - 'featureType': 'landscape.man_made', - 'elementType': 'geometry', - 'stylers': [{ - 'hue': '#0077ff' - }, { - 'gamma': 3.1 - }] -}, { - 'featureType': 'water', - 'stylers': [{ - 'hue': '#00ccff' - }, { - 'gamma': 0.44 - }, { - 'saturation': -33 - }] -}, { - 'featureType': 'poi.park', - 'stylers': [{ - 'hue': '#44ff00' - }, { - 'saturation': -23 - }] -}, { - 'featureType': 'water', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'hue': '#007fff' - }, { - 'gamma': 0.77 - }, { - 'saturation': 65 - }, { - 'lightness': 99 - }] -}, { - 'featureType': 'water', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'gamma': 0.11 - }, { - 'weight': 5.6 - }, { - 'saturation': 99 - }, { - 'hue': '#0091ff' - }, { - 'lightness': -86 - }] -}, { - 'featureType': 'transit.line', - 'elementType': 'geometry', - 'stylers': [{ - 'lightness': -48 - }, { - 'hue': '#ff5e00' - }, { - 'gamma': 1.2 - }, { - 'saturation': -23 - }] -}, { - 'featureType': 'transit', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'saturation': -64 - }, { - 'hue': '#ff9100' - }, { - 'lightness': 16 - }, { - 'gamma': 0.47 - }, { - 'weight': 2.7 - }] -}] -var darkStyle = [{ - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'saturation': 36 - }, { - 'color': '#b39964' - }, { - 'lightness': 40 - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'visibility': 'on' - }, { - 'color': '#000000' - }, { - 'lightness': 16 - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'administrative', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 20 - }] -}, { - 'featureType': 'administrative', - 'elementType': 'geometry.stroke', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 17 - }, { - 'weight': 1.2 - }] -}, { - 'featureType': 'landscape', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 20 - }] -}, { - 'featureType': 'poi', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 21 - }] -}, { - 'featureType': 'road.highway', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 17 - }] -}, { - 'featureType': 'road.highway', - 'elementType': 'geometry.stroke', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 29 - }, { - 'weight': 0.2 - }] -}, { - 'featureType': 'road.arterial', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 18 - }] -}, { - 'featureType': 'road.local', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#181818' - }, { - 'lightness': 16 - }] -}, { - 'featureType': 'transit', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 19 - }] -}, { - 'featureType': 'water', - 'elementType': 'geometry', - 'stylers': [{ - 'lightness': 17 - }, { - 'color': '#525252' - }] -}] -var pGoStyle = [{ - 'featureType': 'landscape.man_made', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#a1f199' - }] -}, { - 'featureType': 'landscape.natural.landcover', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#37bda2' - }] -}, { - 'featureType': 'landscape.natural.terrain', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#37bda2' - }] -}, { - 'featureType': 'poi.attraction', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'visibility': 'on' - }] -}, { - 'featureType': 'poi.business', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#e4dfd9' - }] -}, { - 'featureType': 'poi.business', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'poi.park', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#37bda2' - }] -}, { - 'featureType': 'road', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#84b09e' - }] -}, { - 'featureType': 'road', - 'elementType': 'geometry.stroke', - 'stylers': [{ - 'color': '#fafeb8' - }, { - 'weight': '1.25' - }] -}, { - 'featureType': 'road.highway', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'water', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#5ddad6' - }] -}] -var light2StyleNoLabels = [{ - 'elementType': 'geometry', - 'stylers': [{ - 'hue': '#ff4400' - }, { - 'saturation': -68 - }, { - 'lightness': -4 - }, { - 'gamma': 0.72 - }] -}, { - 'featureType': 'road', - 'elementType': 'labels.icon' -}, { - 'featureType': 'landscape.man_made', - 'elementType': 'geometry', - 'stylers': [{ - 'hue': '#0077ff' - }, { - 'gamma': 3.1 - }] -}, { - 'featureType': 'water', - 'stylers': [{ - 'hue': '#00ccff' - }, { - 'gamma': 0.44 - }, { - 'saturation': -33 - }] -}, { - 'featureType': 'poi.park', - 'stylers': [{ - 'hue': '#44ff00' - }, { - 'saturation': -23 - }] -}, { - 'featureType': 'water', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'hue': '#007fff' - }, { - 'gamma': 0.77 - }, { - 'saturation': 65 - }, { - 'lightness': 99 - }] -}, { - 'featureType': 'water', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'gamma': 0.11 - }, { - 'weight': 5.6 - }, { - 'saturation': 99 - }, { - 'hue': '#0091ff' - }, { - 'lightness': -86 - }] -}, { - 'featureType': 'transit.line', - 'elementType': 'geometry', - 'stylers': [{ - 'lightness': -48 - }, { - 'hue': '#ff5e00' - }, { - 'gamma': 1.2 - }, { - 'saturation': -23 - }] -}, { - 'featureType': 'transit', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'saturation': -64 - }, { - 'hue': '#ff9100' - }, { - 'lightness': 16 - }, { - 'gamma': 0.47 - }, { - 'weight': 2.7 - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}] -var darkStyleNoLabels = [{ - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'administrative', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 20 - }] -}, { - 'featureType': 'administrative', - 'elementType': 'geometry.stroke', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 17 - }, { - 'weight': 1.2 - }] -}, { - 'featureType': 'landscape', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 20 - }] -}, { - 'featureType': 'poi', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 21 - }] -}, { - 'featureType': 'road.highway', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 17 - }] -}, { - 'featureType': 'road.highway', - 'elementType': 'geometry.stroke', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 29 - }, { - 'weight': 0.2 - }] -}, { - 'featureType': 'road.arterial', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 18 - }] -}, { - 'featureType': 'road.local', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#181818' - }, { - 'lightness': 16 - }] -}, { - 'featureType': 'transit', - 'elementType': 'geometry', - 'stylers': [{ - 'color': '#000000' - }, { - 'lightness': 19 - }] -}, { - 'featureType': 'water', - 'elementType': 'geometry', - 'stylers': [{ - 'lightness': 17 - }, { - 'color': '#525252' - }] -}] -var pGoStyleNoLabels = [{ - 'featureType': 'landscape.man_made', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#a1f199' - }] -}, { - 'featureType': 'landscape.natural.landcover', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#37bda2' - }] -}, { - 'featureType': 'landscape.natural.terrain', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#37bda2' - }] -}, { - 'featureType': 'poi.attraction', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'visibility': 'on' - }] -}, { - 'featureType': 'poi.business', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#e4dfd9' - }] -}, { - 'featureType': 'poi.business', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'poi.park', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#37bda2' - }] -}, { - 'featureType': 'road', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#84b09e' - }] -}, { - 'featureType': 'road', - 'elementType': 'geometry.stroke', - 'stylers': [{ - 'color': '#fafeb8' - }, { - 'weight': '1.25' - }] -}, { - 'featureType': 'road.highway', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'water', - 'elementType': 'geometry.fill', - 'stylers': [{ - 'color': '#5ddad6' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [{ - 'visibility': 'off' - }] -}, { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [{ - 'visibility': 'off' - }] -}] var selectedStyle = 'light' -var mapData = { - pokemons: {}, - gyms: {}, - pokestops: {}, - lurePokemons: {}, - scanned: {}, - spawnpoints: {} -} + var gymTypes = ['Uncontested', 'Mystic', 'Valor', 'Instinct'] var audio = new Audio('static/sounds/ding.mp3') -var pokemonSprites = { - normal: { - columns: 12, - iconWidth: 30, - iconHeight: 30, - spriteWidth: 360, - spriteHeight: 390, - filename: 'static/icons-sprite.png', - name: 'Normal' - }, - highres: { - columns: 7, - iconWidth: 65, - iconHeight: 65, - spriteWidth: 455, - spriteHeight: 1430, - filename: 'static/icons-large-sprite.png', - name: 'High-Res' - }, - shuffle: { - columns: 7, - iconWidth: 65, - iconHeight: 65, - spriteWidth: 455, - spriteHeight: 1430, - filename: 'static/icons-shuffle-sprite.png', - name: 'Shuffle' - } -} - -// -// LocalStorage helpers -// - -var StoreTypes = { - Boolean: { - parse: function (str) { - switch (str.toLowerCase()) { - case '1': - case 'true': - case 'yes': - return true - default: - return false - } - }, - stringify: function (b) { - return b ? 'true' : 'false' - } - }, - JSON: { - parse: function (str) { - return JSON.parse(str) - }, - stringify: function (json) { - return JSON.stringify(json) - } - }, - String: { - parse: function (str) { - return str - }, - stringify: function (str) { - return str - } - }, - Number: { - parse: function (str) { - return parseInt(str, 10) - }, - stringify: function (number) { - return number.toString() - } - } -} - -var StoreOptions = { - 'map_style': { - default: 'roadmap', - type: StoreTypes.String - }, - 'remember_select_exclude': { - default: [], - type: StoreTypes.JSON - }, - 'remember_select_notify': { - default: [], - type: StoreTypes.JSON - }, - 'remember_select_rarity_notify': { - default: [], - type: StoreTypes.JSON - }, - 'showGyms': { - default: false, - type: StoreTypes.Boolean - }, - 'showPokemon': { - default: true, - type: StoreTypes.Boolean - }, - 'showPokestops': { - default: true, - type: StoreTypes.Boolean - }, - 'showLuredPokestopsOnly': { - default: 0, - type: StoreTypes.Number - }, - 'showScanned': { - default: false, - type: StoreTypes.Boolean - }, - 'showSpawnpoints': { - default: false, - type: StoreTypes.Boolean - }, - 'showRanges': { - default: false, - type: StoreTypes.Boolean - }, - 'playSound': { - default: false, - type: StoreTypes.Boolean - }, - 'geoLocate': { - default: false, - type: StoreTypes.Boolean - }, - 'lockMarker': { - default: isTouchDevice(), // default to true if touch device - type: StoreTypes.Boolean - }, - 'startAtUserLocation': { - default: false, - type: StoreTypes.Boolean - }, - 'followMyLocation': { - default: false, - type: StoreTypes.Boolean - }, - 'followMyLocationPosition': { - default: [], - type: StoreTypes.JSON - }, - 'pokemonIcons': { - default: 'highres', - type: StoreTypes.String - }, - 'iconSizeModifier': { - default: 0, - type: StoreTypes.Number - }, - 'searchMarkerStyle': { - default: 'google', - type: StoreTypes.String - }, - 'locationMarkerStyle': { - default: 'none', - type: StoreTypes.String - }, - 'zoomLevel': { - default: 16, - type: StoreTypes.Number - } -} - -var Store = { - getOption: function (key) { - var option = StoreOptions[key] - if (!option) { - throw new Error('Store key was not defined ' + key) - } - return option - }, - get: function (key) { - var option = this.getOption(key) - var optionType = option.type - var rawValue = localStorage[key] - if (rawValue === null || rawValue === undefined) { - return option.default - } - var value = optionType.parse(rawValue) - return value - }, - set: function (key, value) { - var option = this.getOption(key) - var optionType = option.type || StoreTypes.String - var rawValue = optionType.stringify(value) - localStorage[key] = rawValue - }, - reset: function (key) { - localStorage.removeItem(key) - } -} // // Functions @@ -1280,26 +483,6 @@ function spawnpointLabel (item) { return str } -function getGoogleSprite (index, sprite, displayHeight) { - displayHeight = Math.max(displayHeight, 3) - var scale = displayHeight / sprite.iconHeight - // Crop icon just a tiny bit to avoid bleedover from neighbor - var scaledIconSize = new google.maps.Size(scale * sprite.iconWidth - 1, scale * sprite.iconHeight - 1) - var scaledIconOffset = new google.maps.Point( - (index % sprite.columns) * sprite.iconWidth * scale + 0.5, - Math.floor(index / sprite.columns) * sprite.iconHeight * scale + 0.5) - var scaledSpriteSize = new google.maps.Size(scale * sprite.spriteWidth, scale * sprite.spriteHeight) - var scaledIconCenterOffset = new google.maps.Point(scale * sprite.iconWidth / 2, scale * sprite.iconHeight / 2) - - return { - url: sprite.filename, - size: scaledIconSize, - scaledSize: scaledSpriteSize, - origin: scaledIconOffset, - anchor: scaledIconCenterOffset - } -} - function addRangeCircle (marker, map, type, teamId) { var targetmap = null var circleCenter = new google.maps.LatLng(marker.position.lat(), marker.position.lng()) @@ -1347,29 +530,7 @@ function isRangeActive (map) { return Store.get('showRanges') } -function setupPokemonMarker (item, skipNotification, isBounceDisabled) { - // Scale icon size up with the map exponentially - var iconSize = 2 + (map.getZoom() - 3) * (map.getZoom() - 3) * 0.2 + Store.get('iconSizeModifier') - var pokemonIndex = item['pokemon_id'] - 1 - var sprite = pokemonSprites[Store.get('pokemonIcons')] || pokemonSprites['highres'] - var icon = getGoogleSprite(pokemonIndex, sprite, iconSize) - - var animationDisabled = false - if (isBounceDisabled === true) { - animationDisabled = true - } - - var marker = new google.maps.Marker({ - position: { - lat: item['latitude'], - lng: item['longitude'] - }, - zIndex: 9999, - map: map, - icon: icon, - animationDisabled: animationDisabled - }) - +function customizePokemonMarker (marker, item, skipNotification) { marker.addListener('click', function () { this.setAnimation(null) this.animationDisabled = true @@ -1397,7 +558,6 @@ function setupPokemonMarker (item, skipNotification, isBounceDisabled) { } addListeners(marker) - return marker } function setupGymMarker (item) { @@ -1739,7 +899,8 @@ function processPokemons (i, item) { item.marker.setMap(null) } if (!item.hidden) { - item.marker = setupPokemonMarker(item) + item.marker = setupPokemonMarker(item, map) + customizePokemonMarker(item.marker, item) mapData.pokemons[item['encounter_id']] = item } } @@ -1883,7 +1044,8 @@ function redrawPokemon (pokemonList) { var item = pokemonList[key] if (!item.hidden) { if (item.marker.rangeCircle) item.marker.rangeCircle.setMap(null) - var newMarker = setupPokemonMarker(item, skipNotification, this.marker.animationDisabled) + var newMarker = setupPokemonMarker(item, map, this.marker.animationDisabled) + customizePokemonMarker(newMarker, item, skipNotification) item.marker.setMap(null) pokemonList[key].marker = newMarker } @@ -2058,11 +1220,6 @@ function i8ln (word) { } } -function isTouchDevice () { - // Should cover most browsers - return 'ontouchstart' in window || navigator.maxTouchPoints -} - // // Page Ready Exection // diff --git a/static/js/statistics.js b/static/js/statistics.js index 06529f94d7..bf37bf9a29 100644 --- a/static/js/statistics.js +++ b/static/js/statistics.js @@ -49,9 +49,8 @@ function addElement (pokemonId, name) { class: 'image' }).appendTo('#seen_' + pokemonId + '_base') - jQuery('', { - src: 'static/icons/' + pokemonId + '.png', - alt: 'Image for Pokemon #' + pokemonId + jQuery('', { + class: 'pokemon-sprite n' + pokemonId }).appendTo(imageContainer) var baseDetailContainer = jQuery('
', { @@ -165,8 +164,7 @@ function processSeen (seen) { document.getElementById('seen_total').innerHTML = 'Total: ' + total.toLocaleString() } -// Override UpdateMap in map.js to take advantage of a pre-existing interval. -function updateMap (firstRun) { +function updateStatMap (firstRun) { var duration = document.getElementById('duration') var header = 'Pokemon Seen in ' + duration.options[duration.selectedIndex].text if ($('#seen_header').html() !== header) { @@ -185,19 +183,16 @@ function updateMap (firstRun) { }) } -updateMap() +updateStatMap() /* Overlay */ var detailsLoading = false var appearancesTimesLoading = false -var detailInterval = null -var lastappearance = 1 var pokemonid = 0 var mapLoaded = false var detailsPersist = false var map = null var heatmap = null -var heatmapNumPoints = -1 var heatmapPoints = [] mapData.appearances = {} @@ -212,7 +207,6 @@ function loadDetails () { 'scanned': false, 'appearances': true, 'pokemonid': pokemonid, - 'last': lastappearance, 'duration': $('#duration').val() }, dataType: 'json', @@ -270,7 +264,6 @@ function closeTimes () { detailsPersist = false } -// Overrides addListeners in map.js function addListeners (marker) { // eslint-disable-line no-unused-vars marker.addListener('click', function () { showTimes(marker) @@ -360,7 +353,7 @@ function initMap () { }) map.setMapTypeId(Store.get('map_style')) - google.maps.event.addListener(map, 'idle', updateMap) + google.maps.event.addListener(map, 'idle', updateStatMap) mapLoaded = true @@ -376,12 +369,9 @@ function resetMap () { }) heatmapPoints = [] - heatmapNumPoints = 0 if (heatmap) { heatmap.setMap(null) } - - lastappearance = 0 } function showOverlay (id) { @@ -394,14 +384,12 @@ function showOverlay (id) { $('#location_details').show() location.hash = 'overlay_' + pokemonid updateDetails() - detailInterval = window.setInterval(updateDetails, 5000) return false } function closeOverlay () { // eslint-disable-line no-unused-vars $('#location_details').hide() - window.clearInterval(detailInterval) closeTimes() location.hash = '' return false @@ -413,23 +401,21 @@ function processAppearance (i, item) { if (item['marker']) { item['marker'].setMap(null) } - item['marker'] = setupPokemonMarker(item, true) + item['marker'] = setupPokemonMarker(item, map, true) + addListeners(item['marker']) item['marker'].spawnpointId = spawnpointId mapData.appearances[spawnpointId] = item - } else { - mapData.appearances[spawnpointId].count += item['count'] } - - heatmapPoints.push(new google.maps.LatLng(item['latitude'], item['longitude'])) - lastappearance = Math.max(lastappearance, item['disappear_time']) + heatmapPoints.push({location: new google.maps.LatLng(item['latitude'], item['longitude']), weight: parseFloat(item['count'])}) } function redrawAppearances (appearances) { $.each(appearances, function (key, value) { var item = appearances[key] if (!item['hidden']) { - var newMarker = setupPokemonMarker(item, true) + var newMarker = setupPokemonMarker(item, map, true) item['marker'].setMap(null) + addListeners(newMarker) newMarker.spawnpointId = item['spawnpoint_id'] appearances[key].marker = newMarker } @@ -471,19 +457,14 @@ function appearanceTab (item) { function updateDetails () { loadDetails().done(function (result) { $.each(result.appearances, processAppearance) - - // Redraw the heatmap with all the new appearances - if (heatmapNumPoints !== heatmapPoints.length) { - if (heatmap) { - heatmap.setMap(null) - } - heatmap = new google.maps.visualization.HeatmapLayer({ - data: heatmapPoints, - map: map, - radius: 50 - }) - heatmapNumPoints = heatmapPoints.length + if (heatmap) { + heatmap.setMap(null) } + heatmap = new google.maps.visualization.HeatmapLayer({ + data: heatmapPoints, + map: map, + radius: 50 + }) }) } @@ -495,4 +476,4 @@ $('#nav select') .select2({ minimumResultsForSearch: Infinity }) - .on('change', updateMap) + .on('change', updateStatMap) diff --git a/templates/map.html b/templates/map.html index 8a7eb49d63..7e58c9f408 100644 --- a/templates/map.html +++ b/templates/map.html @@ -272,6 +272,7 @@