mirror of https://gitlab.com/bashrc2/epicyon
				
				
				
			
		
			
				
	
	
		
			362 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
| __filename__ = "city.py"
 | |
| __author__ = "Bob Mottram"
 | |
| __license__ = "AGPL3+"
 | |
| __version__ = "1.4.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 6"],
 | |
|         ["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"],
 | |
|         ["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 A51"],
 | |
|         ["Samsung", "Galaxy S10"],
 | |
|         ["Samsung", "Galaxy S10 Plus"],
 | |
|         ["Samsung", "Galaxy S10e"],
 | |
|         ["Samsung", "Galaxy S10 5G"],
 | |
|         ["Samsung", "Galaxy A60"],
 | |
|         ["Samsung", "Note 12"],
 | |
|         ["Samsung", "Note 12 Plus"],
 | |
|         ["Samsung", "Note 11"],
 | |
|         ["Samsung", "Note 11 Plus"],
 | |
|         ["Samsung", "Note 10"],
 | |
|         ["Samsung", "Note 10 Plus"],
 | |
|         ["Samsung", "Galaxy S22 Ultra"],
 | |
|         ["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 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
 |