diff --git a/pogom/app.py b/pogom/app.py index 1d5bb1c327..d3985d53b1 100644 --- a/pogom/app.py +++ b/pogom/app.py @@ -10,7 +10,7 @@ from flask_compress import Compress from datetime import datetime from s2sphere import LatLng -from pogom.dyn_img import get_gym_icon, get_pokemon_icon +from pogom.dyn_img import get_gym_icon, get_pokemon_map_icon, get_pokemon_raw_icon from pogom.pgscout import scout_error, pgscout_encounter from pogom.utils import get_args, get_pokemon_name from bisect import bisect_left @@ -83,12 +83,19 @@ def gym_img(self): return send_file(get_gym_icon(team, level, raidlevel, pkm, is_in_battle), mimetype='image/png') def pokemon_img(self): + raw = 'raw' in request.args pkm = int(request.args.get('pkm')) weather = int(request.args.get('weather')) if 'weather' in request.args else 0 gender = int(request.args.get('gender')) if 'gender' in request.args else None form = int(request.args.get('form')) if 'form' in request.args else None costume = int(request.args.get('costume')) if 'costume' in request.args else None - return send_file(get_pokemon_icon(pkm, weather=weather, gender=gender, form=form, costume=costume), mimetype='image/png') + shiny = 'shiny' in request.args + if raw: + filename = get_pokemon_raw_icon(pkm, gender=gender, form=form, costume=costume, weather=weather, + shiny=shiny) + else: + filename = get_pokemon_map_icon(pkm, weather=weather, gender=gender, form=form, costume=costume) + return send_file(filename, mimetype='image/png') def scout_pokemon(self): args = get_args() diff --git a/pogom/dyn_img.py b/pogom/dyn_img.py index cabe1485c6..5241e562c3 100644 --- a/pogom/dyn_img.py +++ b/pogom/dyn_img.py @@ -23,7 +23,6 @@ path_weather = os.path.join(path_images, 'weather') path_generated = os.path.join(path_images, 'generated') path_generated_gym = os.path.join(path_generated, 'gym') -path_generated_pokemon = os.path.join(path_generated, 'pokemon') egg_images = { 1: os.path.join(path_raid, 'egg_normal.png'), @@ -70,6 +69,100 @@ font_pointsize = 25 +def get_pokemon_raw_icon(pkm, gender=None, form=None, costume=None, weather=None, shiny=False): + if generate_images and pogo_assets: + source, target = pokemon_asset_path(pkm, classifier='icon', gender=gender, form=form, costume=costume, weather=weather, shiny=shiny) + im_lines = ['-fuzz 0.5% -trim +repage' + ' -scale "96x96>" -unsharp 0x1' + ' -background none -gravity center -extent 96x96' + ] + return run_imagemagick(source, im_lines, target) + else: + return os.path.join(path_icons, '{}.png'.format(pkm)) + + +def get_pokemon_map_icon(pkm, weather=None, gender=None, form=None, costume=None): + im_lines = [] + + # Add Pokemon icon + if pogo_assets: + source, target = pokemon_asset_path(pkm, classifier='marker', gender=gender, form=form, costume=costume, + weather=weather) + target_size = 96 + im_lines.append( + '-fuzz 0.5% -trim +repage' + ' -scale "133x133>" -unsharp 0x1' + ' -background none -gravity center -extent 139x139' + ' -background black -alpha background -channel A -blur 0x1 -level 0,10%' + ' -adaptive-resize {size}x{size}' + ' -modulate 100,110'.format(size=target_size) + ) + else: + # Extract pokemon icon from spritesheet + source = path_pokemon_spritesheet + weather_suffix = '_{}'.format(WeatherCondition.Name(weather)) if weather else '' + target_path = os.path.join(path_generated, 'pokemon_spritesheet_marker') + target = os.path.join(target_path, 'pokemon_{}{}.png'.format(pkm, weather_suffix)) + + target_size = pkm_sprites_size + pkm_idx = pkm - 1 + x = (pkm_idx % pkm_sprites_cols) * pkm_sprites_size + y = (pkm_idx / pkm_sprites_cols) * pkm_sprites_size + im_lines.append('-crop {size}x{size}+{x}+{y} +repage'.format(size=target_size, x=x, y=y)) + + if weather: + radius = 20 + x = target_size - radius - 2 + y = radius + 1 + y2 = 1 + im_lines.append( + '-gravity northeast' + ' -fill "#FFFD" -stroke black -draw "circle {x},{y} {x},{y2}"' + ' -draw "image over 1,1 42,42 \'{weather_img}\'"'.format(x=x, y=y, y2=y2, weather_img=weather_images[weather]) + ) + + return run_imagemagick(source, im_lines, target) + + +def get_gym_icon(team, level, raidlevel, pkm, is_in_battle): + level = int(level) + + if not generate_images: + return default_gym_image(team, level, raidlevel, pkm) + + im_lines = ['-font "{}" -pointsize {}'.format(font, font_pointsize)] + if pkm and pkm != 'null': + # Gym with ongoing raid + out_filename = os.path.join(path_generated_gym, "{}_L{}_R{}_P{}.png".format(team, level, raidlevel, pkm)) + im_lines.extend(draw_raid_pokemon(pkm)) + im_lines.extend(draw_raid_level(raidlevel)) + if level > 0: + im_lines.extend(draw_gym_level(level)) + elif raidlevel: + # Gym with upcoming raid (egg) + raidlevel = int(raidlevel) + out_filename = os.path.join(path_generated_gym, "{}_L{}_R{}.png".format(team, level, raidlevel)) + im_lines.extend(draw_raid_egg(raidlevel)) + im_lines.extend(draw_raid_level(raidlevel)) + if level > 0: + im_lines.extend(draw_gym_level(level)) + elif level > 0: + # Occupied gym + out_filename = os.path.join(path_generated_gym, '{}_L{}.png'.format(team, level)) + im_lines.extend(draw_gym_level(level)) + else: + # Neutral gym + return os.path.join(path_gym, '{}.png'.format(team)) + + # Battle Indicator + if is_in_battle: + out_filename = out_filename.replace('.png', '_B.png') + im_lines.extend(draw_battle_indicator()) + + gym_image = os.path.join(path_gym, '{}.png'.format(team)) + return run_imagemagick(gym_image, im_lines, out_filename) + + def draw_raid_pokemon(pkm): if pogo_assets: pkm_path, dummy = pokemon_asset_path(int(pkm)) @@ -85,7 +178,7 @@ def draw_raid_egg(raidlevel): egg_path = os.path.join(pogo_assets, egg_images_assets[raidlevel]) else: egg_path = egg_images[raidlevel] - return draw_gym_subject(egg_path, 36, 'center') + return draw_gym_subject(egg_path, 36, gravity='center') def draw_gym_level(level): @@ -133,100 +226,18 @@ def battle_indicator_swords(): ] -def get_gym_icon(team, level, raidlevel, pkm, is_in_battle): - init_image_dir(path_generated_gym) - level = int(level) - - if not generate_images: - return default_gym_image(team, level, raidlevel, pkm) - - im_lines = ['-font "{}" -pointsize {}'.format(font, font_pointsize)] - if pkm and pkm != 'null': - # Gym with ongoing raid - out_filename = os.path.join(path_generated_gym, "{}_L{}_R{}_P{}.png".format(team, level, raidlevel, pkm)) - im_lines.extend(draw_raid_pokemon(pkm)) - im_lines.extend(draw_raid_level(raidlevel)) - if level > 0: - im_lines.extend(draw_gym_level(level)) - elif raidlevel: - # Gym with upcoming raid (egg) - raidlevel = int(raidlevel) - out_filename = os.path.join(path_generated_gym, "{}_L{}_R{}.png".format(team, level, raidlevel)) - im_lines.extend(draw_raid_egg(raidlevel)) - im_lines.extend(draw_raid_level(raidlevel)) - if level > 0: - im_lines.extend(draw_gym_level(level)) - elif level > 0: - # Occupied gym - out_filename = os.path.join(path_generated_gym, '{}_L{}.png'.format(team, level)) - im_lines.extend(draw_gym_level(level)) - else: - # Neutral gym - return os.path.join(path_gym, '{}.png'.format(team)) - - # Battle Indicator - if is_in_battle: - out_filename = out_filename.replace('.png', '_B.png') - im_lines.extend(draw_battle_indicator()) - - gym_image = os.path.join(path_gym, '{}.png'.format(team)) - return run_imagemagick(gym_image, im_lines, out_filename) - - -def get_pokemon_icon(pkm, weather=None, gender=None, form=None, costume=None): - init_image_dir(path_generated_pokemon) - - im_lines = [] - - # Add Pokemon icon - if pogo_assets: - source, target = pokemon_asset_path(pkm, gender, form, costume, weather) - target_size = 96 - im_lines.append( - '-fuzz 0.5% -trim +repage' - ' -scale "133x133>" -unsharp 0x1' - ' -background none -gravity center -extent 139x139' - ' -background black -alpha background -channel A -blur 0x1 -level 0,10%' - ' -adaptive-resize {size}x{size}' - ' -modulate 100,110'.format(size=target_size) - ) - else: - # Extract pokemon icon from spritesheet - source = path_pokemon_spritesheet - weather_suffix = '_{}'.format(WeatherCondition.Name(weather)) if weather else '' - target = os.path.join(path_generated_pokemon, 'pokemon_{}{}.png'.format(pkm, weather_suffix)) - - target_size = pkm_sprites_size - pkm_idx = pkm - 1 - x = (pkm_idx % pkm_sprites_cols) * pkm_sprites_size - y = (pkm_idx / pkm_sprites_cols) * pkm_sprites_size - im_lines.append('-crop {size}x{size}+{x}+{y} +repage'.format(size=target_size, x=x, y=y)) - - if weather: - radius = 20 - x = target_size - radius - 2 - y = radius + 1 - y2 = 1 - im_lines.append( - '-gravity northeast' - ' -fill "#FFFD" -stroke black -draw "circle {x},{y} {x},{y2}"' - ' -draw "image over 1,1 42,42 \'{weather_img}\'"'.format(x=x, y=y, y2=y2, weather_img=weather_images[weather]) - ) - - return run_imagemagick(source, im_lines, target) - - -def pokemon_asset_path(pkm, gender=GENDER_UNSET, form=None, costume=None, weather=None): +def pokemon_asset_path(pkm, classifier=None, gender=GENDER_UNSET, form=None, costume=None, weather=None, shiny=False): gender_suffix = gender_assets_suffix = '' form_suffix = form_assets_suffix = '' costume_suffix = costume_assets_suffix = '' weather_suffix = '_{}'.format(WeatherCondition.Name(weather)) if weather else '' + shiny_suffix = '_shiny' if shiny else '' if gender in (MALE, FEMALE): gender_assets_suffix = '_{:02d}'.format(gender - 1) gender_suffix = '_{}'.format(Gender.Name(gender)) elif gender in (GENDER_UNSET, GENDERLESS): - gender_assets_suffix = '_00' + gender_assets_suffix = '_00' if pkm > 0 else '' if form: # Form = no gender @@ -239,21 +250,25 @@ def pokemon_asset_path(pkm, gender=GENDER_UNSET, form=None, costume=None, weathe costume_suffix = '_{}'.format(Costume.Name(costume)) if not gender_assets_suffix and not form_assets_suffix and not costume_assets_suffix: - gender_assets_suffix = '_16' if pkm == 201 else '_00' + gender_assets_suffix = '_16' if pkm == 201 else '_00' if pkm > 0 else '' assets_basedir = os.path.join(pogo_assets, 'decrypted_assets') assets_fullname = os.path.join(assets_basedir, - 'pokemon_icon_{:03d}{}{}{}.png'.format(pkm, gender_assets_suffix, form_assets_suffix, - costume_assets_suffix)) - target_name = os.path.join(path_generated_pokemon, - "pkm_{}{}{}{}{}.png".format(pkm, gender_suffix, form_suffix, costume_suffix, - weather_suffix)) + 'pokemon_icon_{:03d}{}{}{}{}.png'.format(pkm, gender_assets_suffix, form_assets_suffix, + costume_assets_suffix, shiny_suffix)) + target_path = os.path.join(path_generated, 'pokemon_{}'.format(classifier)) if classifier else os.path.join( + path_generated, 'pokemon') + target_name = os.path.join(target_path, + "pkm_{:03d}{}{}{}{}{}.png".format(pkm, gender_suffix, form_suffix, costume_suffix, + weather_suffix, shiny_suffix)) if os.path.isfile(assets_fullname): return assets_fullname, target_name else: if gender == MALE: - raise Exception("Cannot find PogoAssets file {}".format(assets_fullname)) - return pokemon_asset_path(pkm, MALE, form, costume, weather) + log.warning("Cannot find PogoAssets file {}".format(assets_fullname)) + # Dummy Pokemon icon + return os.path.join(assets_basedir, 'pokemon_icon_000.png'), os.path.join(target_path, 'pkm_000.png') + return pokemon_asset_path(pkm, classifier=classifier, gender=MALE, form=form, costume=costume, weather=weather) def draw_gym_subject(image, size, gravity='north', trim=False): @@ -300,6 +315,9 @@ def default_gym_image(team, level, raidlevel, pkm): def run_imagemagick(source, im_lines, out_filename): if not os.path.isfile(out_filename): + # Make sure, target path exists + init_image_dir(os.path.split(out_filename)[0]) + cmd = '{} "{}" {} "{}"'.format(imagemagick_executable, source, join(im_lines), out_filename) if os.name != 'nt': cmd = cmd.replace(" ( ", " \( ").replace(" ) ", " \) ") diff --git a/pogom/models.py b/pogom/models.py index a6b2f61fc3..234ce3817b 100644 --- a/pogom/models.py +++ b/pogom/models.py @@ -48,7 +48,7 @@ flaskDb = FlaskDB() cache = TTLCache(maxsize=100, ttl=60 * 5) -db_schema_version = 24 +db_schema_version = 25 class MyRetryDB(RetryOperationalError, PooledMySQLDatabase): @@ -463,6 +463,11 @@ class Gym(LatLongModel): longitude = DoubleField() total_cp = SmallIntegerField() is_in_battle = BooleanField() + gender = SmallIntegerField(null=True) + form = SmallIntegerField(null=True) + costume = SmallIntegerField(null=True) + weather_boosted_condition = SmallIntegerField(null=True) + shiny = BooleanField(null=True) last_modified = DateTimeField(index=True) last_scanned = DateTimeField(default=datetime.utcnow, index=True) @@ -533,6 +538,11 @@ def get_gyms(swLat, swLng, neLat, neLng, timestamp=0, oSwLat=None, GymMember.deployment_time, GymMember.last_scanned, GymPokemon.pokemon_id, + GymPokemon.gender, + GymPokemon.form, + GymPokemon.costume, + GymPokemon.weather_boosted_condition, + GymPokemon.shiny, Trainer.name.alias('trainer_name'), Trainer.level.alias('trainer_level')) .join(Gym, on=(GymMember.gym_id == Gym.gym_id)) @@ -585,6 +595,11 @@ def get_gym(id): GymDetails.name, GymDetails.description, Gym.guard_pokemon_id, + Gym.gender, + Gym.form, + Gym.costume, + Gym.weather_boosted_condition, + Gym.shiny, Gym.slots_available, Gym.latitude, Gym.longitude, @@ -616,6 +631,11 @@ def get_gym(id): GymPokemon.iv_attack, GymPokemon.iv_defense, GymPokemon.iv_stamina, + GymPokemon.gender, + GymPokemon.form, + GymPokemon.costume, + GymPokemon.weather_boosted_condition, + GymPokemon.shiny, Trainer.name.alias('trainer_name'), Trainer.level.alias('trainer_level')) .join(Gym, on=(GymMember.gym_id == Gym.gym_id)) @@ -1775,6 +1795,11 @@ class GymPokemon(BaseModel): iv_defense = SmallIntegerField(null=True) iv_stamina = SmallIntegerField(null=True) iv_attack = SmallIntegerField(null=True) + gender = SmallIntegerField(null=True) + form = SmallIntegerField(null=True) + costume = SmallIntegerField(null=True) + weather_boosted_condition = SmallIntegerField(null=True) + shiny = BooleanField(null=True) last_seen = DateTimeField(default=datetime.utcnow) @@ -2430,6 +2455,16 @@ def parse_map(args, map_dict, scan_coords, scan_location, db_update_queue, f.owned_by_team, 'guard_pokemon_id': f.guard_pokemon_id, + 'gender': + f.guard_pokemon_display.gender, + 'form': + f.guard_pokemon_display.form, + 'costume': + f.guard_pokemon_display.costume, + 'weather_boosted_condition': + f.guard_pokemon_display.weather_boosted_condition, + 'shiny': + f.guard_pokemon_display.shiny, 'slots_available': gym_display.slots_available, 'total_cp': @@ -2785,6 +2820,11 @@ def parse_gyms(args, gym_responses, wh_update_queue, db_update_queue): 'iv_defense': pokemon.individual_defense, 'iv_stamina': pokemon.individual_stamina, 'iv_attack': pokemon.individual_attack, + 'gender': pokemon.pokemon_display.gender, + 'form': pokemon.pokemon_display.form, + 'costume': pokemon.pokemon_display.costume, + 'weather_boosted_condition': pokemon.pokemon_display.weather_boosted_condition, + 'shiny': pokemon.pokemon_display.shiny, 'last_seen': datetime.utcnow(), } @@ -3346,5 +3386,29 @@ def database_migrate(db, old_ver): SmallIntegerField(null=True)) ) + if old_ver < 25: + migrate( + migrator.add_column('gympokemon', 'gender', + SmallIntegerField(null=True)), + migrator.add_column('gympokemon', 'form', + SmallIntegerField(null=True)), + migrator.add_column('gympokemon', 'costume', + SmallIntegerField(null=True)), + migrator.add_column('gympokemon', 'weather_boosted_condition', + SmallIntegerField(null=True)), + migrator.add_column('gympokemon', 'shiny', + BooleanField(null=True)), + migrator.add_column('gym', 'gender', + SmallIntegerField(null=True)), + migrator.add_column('gym', 'form', + SmallIntegerField(null=True)), + migrator.add_column('gym', 'costume', + SmallIntegerField(null=True)), + migrator.add_column('gym', 'weather_boosted_condition', + SmallIntegerField(null=True)), + migrator.add_column('gym', 'shiny', + BooleanField(null=True)) + ) + # Always log that we're done. log.info('Schema upgrade complete.') diff --git a/static/js/map.common.js b/static/js/map.common.js index ee62e78bb5..8b0b8364f1 100644 --- a/static/js/map.common.js +++ b/static/js/map.common.js @@ -1253,3 +1253,18 @@ function cssPercentageCircle(text, value, perfect_val, good_val, ok_val, meh_val ` } + +function get_pokemon_raw_icon_url(p) { + if (!generateImages) { + return `static/icons/${p.pokemon_id}.png` + } + var url = 'pkm_img?raw=1&pkm=' + p.pokemon_id + var props = ['gender', 'form', 'costume', 'shiny'] + for (var i = 0; i < props.length; i++) { + var prop = props[i] + if (prop in p && p[prop] != null && p[prop]) { + url += '&' + prop + '=' + p[prop] + } + } + return url +} diff --git a/static/js/map.js b/static/js/map.js index b9a08cf033..aced6c144e 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -662,6 +662,8 @@ function pokemonLabel(item) { var hideLabel = excludedPokemon.indexOf(id) < 0 ? "Hide" : "Unhide" var notifyLabel = notifiedPokemon.indexOf(id) < 0 ? "Notify" : "Unnotify" + var pokemon_icon = get_pokemon_raw_icon_url(item) + if (cp !== null && cpMultiplier !== null) { var pokemonLevel = getPokemonLevel(cpMultiplier) @@ -676,7 +678,7 @@ function pokemonLabel(item) {
@@ -2358,11 +2366,11 @@ function getSidebarGymMember(pokemon) { var perfectPercent = getIv(pokemon.iv_attack, pokemon.iv_defense, pokemon.iv_stamina) var moveEnergy = Math.round(100 / pokemon.move_2_energy) - + var pokemon_image = get_pokemon_raw_icon_url(pokemon) return `