__filename__ = "city.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.5.0" __maintainer__ = "Bob Mottram" __email__ = "bob@libreserver.org" __status__ = "Production" __module_group__ = "Metadata" import os import datetime import random import math from random import randint from utils import acct_dir from utils import remove_eol # states which the simulated city dweller can be in PERSON_SLEEP = 0 PERSON_WORK = 1 PERSON_PLAY = 2 PERSON_SHOP = 3 PERSON_EVENING = 4 PERSON_PARTY = 5 BUSY_STATES = (PERSON_WORK, PERSON_SHOP, PERSON_PLAY, PERSON_PARTY) def _get_decoy_camera(decoy_seed: int) -> (str, str, int): """Returns a decoy camera make and model which took the photo """ cameras = [ ["Apple", "iPhone SE"], ["Apple", "iPhone XR"], ["Apple", "iPhone 7"], ["Apple", "iPhone 8"], ["Apple", "iPhone 11"], ["Apple", "iPhone 11 Pro"], ["Apple", "iPhone 12"], ["Apple", "iPhone 12 Mini"], ["Apple", "iPhone 12 Pro Max"], ["Apple", "iPhone 13"], ["Apple", "iPhone 13 Mini"], ["Apple", "iPhone 13 Pro"], ["Apple", "iPhone 14"], ["Apple", "iPhone 14 Pro"], ["Samsung", "Galaxy Note 20 Ultra"], ["Samsung", "Galaxy S20 Plus"], ["Samsung", "Galaxy S20 FE 5G"], ["Samsung", "Galaxy Z FOLD 2"], ["Samsung", "Galaxy S12 Plus"], ["Samsung", "Galaxy S12"], ["Samsung", "Galaxy S11 Plus"], ["Samsung", "Galaxy S10 Plus"], ["Samsung", "Galaxy S10e"], ["Samsung", "Galaxy Z Flip"], ["Samsung", "Galaxy A54"], ["Samsung", "Galaxy A51"], ["Samsung", "Galaxy S10"], ["Samsung", "Galaxy S10 Plus"], ["Samsung", "Galaxy S10e"], ["Samsung", "Galaxy S10 5G"], ["Samsung", "Galaxy A60"], ["Samsung", "Note 13"], ["Samsung", "Note 13 Plus"], ["Samsung", "Note 12"], ["Samsung", "Note 12 Plus"], ["Samsung", "Note 11"], ["Samsung", "Note 11 Plus"], ["Samsung", "Note 10"], ["Samsung", "Note 10 Plus"], ["Samsung", "Galaxy S23 Ultra"], ["Samsung", "Galaxy S23"], ["Samsung", "Galaxy S22 Ultra"], ["Samsung", "Galaxy S22"], ["Samsung", "Galaxy S21 Ultra"], ["Samsung", "Galaxy Note 20 Ultra"], ["Samsung", "Galaxy S21"], ["Samsung", "Galaxy S21 Plus"], ["Samsung", "Galaxy S20 FE"], ["Samsung", "Galaxy Z Fold 2"], ["Samsung", "Galaxy A52 5G"], ["Samsung", "Galaxy A71 5G"], ["Google", "Pixel 7 Pro"], ["Google", "Pixel 7"], ["Google", "Pixel 6 Pro"], ["Google", "Pixel 6"], ["Google", "Pixel 5"], ["Google", "Pixel 4a"], ["Google", "Pixel 4 XL"], ["Google", "Pixel 3 XL"], ["Google", "Pixel 4"], ["Google", "Pixel 4a 5G"], ["Google", "Pixel 3"], ["Google", "Pixel 3a"] ] randgen = random.Random(decoy_seed) index = randgen.randint(0, len(cameras) - 1) serial_number = randgen.randint(100000000000, 999999999999999999999999) return cameras[index][0], cameras[index][1], serial_number def _get_city_pulse(curr_time_of_day, decoy_seed: int) -> (float, float): """This simulates expected average patterns of movement in a city. Jane or Joe average lives and works in the city, commuting in and out of the central district for work. They have a unique life pattern, which machine learning can latch onto. This returns a polar coordinate for the simulated city dweller: Distance from the city centre is in the range 0.0 - 1.0 Angle is in radians """ randgen = random.Random(decoy_seed) variance = 3 data_decoy_state = PERSON_SLEEP weekday = curr_time_of_day.weekday() min_hour = 7 + randint(0, variance) max_hour = 17 + randint(0, variance) if curr_time_of_day.hour > min_hour: if curr_time_of_day.hour <= max_hour: if weekday < 5: data_decoy_state = PERSON_WORK elif weekday == 5: data_decoy_state = PERSON_SHOP else: data_decoy_state = PERSON_PLAY else: if weekday < 5: data_decoy_state = PERSON_EVENING else: data_decoy_state = PERSON_PARTY randgen2 = random.Random(decoy_seed + data_decoy_state) angle_radians = \ (randgen2.randint(0, 100000) / 100000) * 2 * math.pi # some people are quite random, others have more predictable habits decoy_randomness = randgen.randint(1, 3) # occasionally throw in a wildcard to keep the machine learning guessing if randint(0, 100) < decoy_randomness: distance_from_city_center = randint(0, 100000) / 100000 angle_radians = (randint(0, 100000) / 100000) * 2 * math.pi else: # what consitutes the central district is fuzzy central_district_fuzz = (randgen.randint(0, 100000) / 100000) * 0.1 busy_radius = 0.3 + central_district_fuzz if data_decoy_state in BUSY_STATES: # if we are busy then we're somewhere in the city center distance_from_city_center = \ (randgen.randint(0, 100000) / 100000) * busy_radius else: # otherwise we're in the burbs distance_from_city_center = busy_radius + \ ((1.0 - busy_radius) * (randgen.randint(0, 100000) / 100000)) return distance_from_city_center, angle_radians def parse_nogo_string(nogo_line: str) -> []: """Parses a line from locations_nogo.txt and returns the polygon """ nogo_line = remove_eol(nogo_line) polygon_str = nogo_line.split(':', 1)[1] if ';' in polygon_str: pts = polygon_str.split(';') else: pts = polygon_str.split(',') if len(pts) <= 4: return [] polygon = [] for index in range(int(len(pts)/2)): if index*2 + 1 >= len(pts): break longitude_str = pts[index*2].strip() latitude_str = pts[index*2 + 1].strip() if 'E' in latitude_str or 'W' in latitude_str: longitude_str = pts[index*2 + 1].strip() latitude_str = pts[index*2].strip() if 'E' in longitude_str: longitude_str = \ longitude_str.replace('E', '') longitude = float(longitude_str) elif 'W' in longitude_str: longitude_str = \ longitude_str.replace('W', '') longitude = -float(longitude_str) else: longitude = float(longitude_str) latitude = float(latitude_str) polygon.append([latitude, longitude]) return polygon def spoof_geolocation(base_dir: str, city: str, curr_time, decoy_seed: int, cities_list: [], nogo_list: []) -> (float, float, str, str, str, str, int): """Given a city and the current time spoofs the location for an image returns latitude, longitude, N/S, E/W, camera make, camera model, camera serial number """ locations_filename = base_dir + '/custom_locations.txt' if not os.path.isfile(locations_filename): locations_filename = base_dir + '/locations.txt' nogo_filename = base_dir + '/custom_locations_nogo.txt' if not os.path.isfile(nogo_filename): nogo_filename = base_dir + '/locations_nogo.txt' man_city_radius = 0.1 variance_at_location = 0.0004 default_latitude = 51.8744 default_longitude = 0.368333 default_latdirection = 'N' default_longdirection = 'W' if cities_list: cities = cities_list else: if not os.path.isfile(locations_filename): return (default_latitude, default_longitude, default_latdirection, default_longdirection, "", "", 0) cities = [] try: with open(locations_filename, 'r', encoding='utf-8') as loc_file: cities = loc_file.readlines() except OSError: print('EX: unable to read locations ' + locations_filename) nogo = [] if nogo_list: nogo = nogo_list else: if os.path.isfile(nogo_filename): nogo_list = [] try: with open(nogo_filename, 'r', encoding='utf-8') as nogo_file: nogo_list = nogo_file.readlines() except OSError: print('EX: unable to read ' + nogo_filename) for line in nogo_list: if line.startswith(city + ':'): polygon = parse_nogo_string(line) if polygon: nogo.append(polygon) city = city.lower() for city_name in cities: if city in city_name.lower(): city_fields = city_name.split(':') latitude = city_fields[1] longitude = city_fields[2] area_km2 = 0 if len(city_fields) > 3: area_km2 = int(city_fields[3]) latdirection = 'N' longdirection = 'E' if 'S' in latitude: latdirection = 'S' latitude = latitude.replace('S', '') if 'W' in longitude: longdirection = 'W' longitude = longitude.replace('W', '') latitude = float(latitude) longitude = float(longitude) # get the time of day at the city approx_time_zone = int(longitude / 15.0) if longdirection == 'E': approx_time_zone = -approx_time_zone curr_time_adjusted = curr_time - \ datetime.timedelta(hours=approx_time_zone) cam_make, cam_model, cam_serial_number = \ _get_decoy_camera(decoy_seed) valid_coord = False seed_offset = 0 while not valid_coord: # patterns of activity change in the city over time (distance_from_city_center, angle_radians) = \ _get_city_pulse(curr_time_adjusted, decoy_seed + seed_offset) # The city radius value is in longitude and the reference # is Manchester. Adjust for the radius of the chosen city. if area_km2 > 1: man_radius = math.sqrt(1276 / math.pi) radius = math.sqrt(area_km2 / math.pi) city_radius_deg = (radius / man_radius) * man_city_radius else: city_radius_deg = man_city_radius # Get the position within the city, with some randomness added latitude += \ distance_from_city_center * city_radius_deg * \ math.cos(angle_radians) longitude += \ distance_from_city_center * city_radius_deg * \ math.sin(angle_radians) longval = longitude if longdirection == 'W': longval = -longitude valid_coord = not point_in_nogo(nogo, latitude, longval) if not valid_coord: seed_offset += 1 if seed_offset > 100: break # add a small amount of variance around the location fraction = randint(0, 100000) / 100000 distance_from_location = fraction * fraction * variance_at_location fraction = randint(0, 100000) / 100000 angle_from_location = fraction * 2 * math.pi latitude += distance_from_location * math.cos(angle_from_location) longitude += distance_from_location * math.sin(angle_from_location) # gps locations aren't transcendental, so round to a fixed # number of decimal places latitude = int(latitude * 100000) / 100000.0 longitude = int(longitude * 100000) / 100000.0 return (latitude, longitude, latdirection, longdirection, cam_make, cam_model, cam_serial_number) return (default_latitude, default_longitude, default_latdirection, default_longdirection, "", "", 0) def get_spoofed_city(city: str, base_dir: str, nickname: str, domain: str) -> str: """Returns the name of the city to use as a GPS spoofing location for image metadata """ city = '' city_filename = acct_dir(base_dir, nickname, domain) + '/city.txt' if os.path.isfile(city_filename): try: with open(city_filename, 'r', encoding='utf-8') as city_file: city1 = city_file.read() city = remove_eol(city1) except OSError: print('EX: unable to read ' + city_filename) return city def _point_in_polygon(poly: [], x_coord: float, y_coord: float) -> bool: """Returns true if the given point is inside the given polygon """ num = len(poly) inside = False p2x = 0.0 p2y = 0.0 xints = 0.0 p1x, p1y = poly[0] for i in range(num + 1): p2x, p2y = poly[i % num] if y_coord > min(p1y, p2y): if y_coord <= max(p1y, p2y): if x_coord <= max(p1x, p2x): if p1y != p2y: xints = \ (y_coord - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x_coord <= xints: inside = not inside p1x, p1y = p2x, p2y return inside def point_in_nogo(nogo: [], latitude: float, longitude: float) -> bool: """Returns true of the given geolocation is within a nogo area """ for polygon in nogo: if _point_in_polygon(polygon, latitude, longitude): return True return False