diff --git a/.gitignore b/.gitignore index a0ad585..7f787ce 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,11 @@ ENV/ .spyderproject # Rope project settings -.ropeproject \ No newline at end of file +.ropeproject + +# Google API Key +ground_station/resources/core/key + +# Map Images +*.jpg +*.png diff --git a/ground_station/.idea/workspace.xml b/ground_station/.idea/workspace.xml deleted file mode 100644 index 20e90b8..0000000 --- a/ground_station/.idea/workspace.xml +++ /dev/null @@ -1,527 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - DEFINITION_ORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - project - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1510962234686 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AAAAsNHwc2N+8Zbshoe1tr2XDVkf8aGU/l0YnN7t0gOfSXYMOeQZo2dVv+GbvgrFH5EF+uviJzRPHArF3uX1Vb0CuhmnSE0NSLdGfSjyMrpWsoZyS3WBI979X8Y/8lSe/g2O65pafrceXlU4ySEbA9w/JNU42PtXijURL7sXfaZ8kxhE5bYQlRppUyBo5g/qgzvj275mbp6uuZ5w9ssFsOzrJGsGulioJmZ9L9P6O6PVj2sPT2AEw+jC8eGiiappc0lWOA== - - \ No newline at end of file diff --git a/ground_station/resources/core/mappingtest.py b/ground_station/resources/core/mappingtest.py new file mode 100755 index 0000000..4a5414a --- /dev/null +++ b/ground_station/resources/core/mappingtest.py @@ -0,0 +1,24 @@ +#!/usr/bin/python + +import mapping +import PIL + +obj = mapping.GMapsStitcher(2000, 2000, 44.567161, -123.278432, 18, 'terrain', None, 20) + +obj.display_image.save("unzoomed.png") +# draw = PIL.ImageDraw.ImageDraw(obj.big_image) +# draw.rectangle([950, 950, 1050, 1050], fill=128) +# lat, lng = obj.move_latlon(44.559919, -123.280723) +# draw.rectangle([lng-300, lat-300, lng+300, lat+300], fill=328) +# obj.big_image.save("toobig.jpg") +# obj.display_image.save("zoomed.jpg") + +obj.add_gps_location(44.559919, -123.280723, "square", 50, (00, 00, 00, 256)) +obj.add_gps_location(44.565094, -123.276110, "square", 50, (00, 00, 00, 256)) +obj.add_gps_location(44.565777, -123.278902, "square", 50, (00, 00, 00, 256)) +obj.display_image.save("box.png") +obj.center_display(44.567161, -123.278432) +obj.display_image.save("centered.png") +obj.big_image.save("toobig.png") + +print obj diff --git a/software/ground_station/Framework/MapSystems/RoverMap.py b/software/ground_station/Framework/MapSystems/RoverMap.py new file mode 100644 index 0000000..e1c8671 --- /dev/null +++ b/software/ground_station/Framework/MapSystems/RoverMap.py @@ -0,0 +1,333 @@ +''' +Mapping.py: Objected Orientated Google Maps for Python +ReWritten by Chris Pham + +Copyright OSURC, orginal code from GooMPy by Alec Singer and Simon D. Levy + +This code is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. +This code is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU Lesser General Public License +along with this code. If not, see . +''' + +##################################### +# Imports +##################################### +# Python native imports +import math +import urllib2 +from io import StringIO, BytesIO +import os +import time +import PIL.ImageDraw +import signing +import MapHelper + +##################################### +# Constants +##################################### +_KEYS = [] +# Number of pixels in half the earth's circumference at zoom = 21 +_EARTHPIX = 268435456 +# Number of decimal places for rounding coordinates +_DEGREE_PRECISION = 4 +# Larget tile we can grab without paying +_TILESIZE = 640 +# Fastest rate at which we can download tiles without paying +_GRABRATE = 4 +# Pixel Radius of Earth for calculations +_PIXRAD = _EARTHPIX / math.pi +_DISPLAYPIX = _EARTHPIX / 2000 + +file_pointer = open('key', 'r') +for i in file_pointer: + _KEYS.append(i.rstrip()) +file_pointer.close() + + +class GMapsStitcher(object): + def __init__(self, width, height, + latitude, longitude, zoom, + maptype, radius_meters=None, num_tiles=4, debug=False): + self.helper = MapHelper.MapHelper() + self.latitude = latitude + self.longitude = longitude + self.start_latitude = latitude + self.start_longitude = longitude + self.width = width + self.height = height + self.zoom = zoom + self.maptype = maptype + self.radius_meters = radius_meters + self.num_tiles = num_tiles + self.display_image = self.helper.new_image(width, height) + self.debug = debug + + # Get the big image here + self._fetch() + self.center_display(latitude, longitude) + + def __str__(self): + """ + This string returns when used in a print statement + Useful for debugging and to print current state + + returns STRING + """ + string_builder = "" + string_builder += ("Center of the displayed map: %4f, %4f\n" % + (self.center_x, self.center_y)) + string_builder += ("Center of the big map: %4fx%4f\n" % + (self.start_longitude, self.start_longitude)) + string_builder += ("Current latitude is: %4f, %4f\n" % + (self.longitude, self.latitude)) + string_builder += ("The top-left of the box: %dx%d\n" % + (self.left_x, self.upper_y)) + string_builder += ("Number of tiles genreated: %dx%d\n" % + (self.num_tiles, self.num_tiles)) + string_builder += "Map Type: %s\n" % (self.maptype) + string_builder += "Zoom Level: %s\n" % (self.zoom) + string_builder += ("Dimensions of Big Image: %dx%d\n" % + (self.big_image.size[0], self.big_image.size[1])) + string_builder += ("Dimensions of Displayed Image: %dx%d\n" % + (self.width, self.height)) + string_builder += ("LatLong of Northwest Corner: %4f, %4f\n" % + (self.northwest)) + string_builder += ("LatLong of Southeast Corner: %4f, %4f\n" % + (self.southeast)) + return string_builder + + def _grab_tile(self, longitude, latitude, sleeptime=0): + """ + This will return the tile at location longitude x latitude. + Includes a sleep time to allow for free use if there is no API key + + returns PIL.IMAGE OBJECT + """ + # Make the url string for polling + # GET request header gets appended to the string + urlbase = 'https://maps.googleapis.com/maps/api/staticmap?' + urlbase += 'center=%.4f,%.4f&zoom=%d&maptype=%s' + urlbase += '&size=%dx%d&format=png&key=%s' + + # Fill the formatting + specs = (self.helper.fast_round(latitude, _DEGREE_PRECISION), + self.helper.fast_round(longitude, _DEGREE_PRECISION), + self.zoom, self.maptype, _TILESIZE, _TILESIZE, _KEYS[0]) + filename = 'Resources/Maps/' + ('%.4f_%.4f_%d_%s_%d_%d_%s' % specs) + filename += '.png' + + # Tile Image object + tile_object = None + + if os.path.isfile(filename): + tile_object = PIL.Image.open(filename) + + # If file on filesystem + else: + # make the url + url = urlbase % specs + url = signing.sign_url(url, _KEYS[1]) + result = urllib2.urlopen(urllib2.Request(url)).read() + tile_object = PIL.Image.open(BytesIO(result)) + if not os.path.exists('Resources/Maps'): + os.mkdir('Resources/Maps') + tile_object.save(filename) + # Added to prevent timeouts on Google Servers + time.sleep(sleeptime) + + return tile_object + + def _pixels_to_lon(self, iterator, lon_pixels): + """ + This converts pixels to degrees to be used in + fetching squares and generate correct squares + + returns FLOAT(degrees) + """ + # Magic Lines, no idea + degrees = self.helper.pixels_to_degrees((iterator - self.num_tiles / 2) + * _TILESIZE, self.zoom) + return math.degrees((lon_pixels + degrees - _EARTHPIX) / _PIXRAD) + + def _pixels_to_lat(self, iterator, lat_pixels): + """ + This converts pixels to latitude using meridian projection + to get the latitude to generate squares + + returns FLOAT(degrees) + """ + # Magic Lines + return math.degrees(math.pi / 2 - 2 * math.atan(math.exp(((lat_pixels + + self.helper.pixels_to_degrees( + (iterator - self.num_tiles / 2) + * _TILESIZE, self.zoom)) + - _EARTHPIX) / _PIXRAD))) + + def fetch_tiles(self): + """ + Function that handles fetching of files from init'd variables + + returns PIL.IMAGE OBJECT, (WEST, NORTH), (EAST, SOUTH) + + North/East/South/West are in FLOAT(degrees) + """ + # cap floats to precision amount + self.latitude = self.helper.fast_round(self.latitude, + _DEGREE_PRECISION) + self.longitude = self.helper.fast_round(self.longitude, + _DEGREE_PRECISION) + + # number of tiles required to go from center + # latitude to desired radius in meters + if self.radius_meters is not None: + self.num_tiles = (int( + round(2 * self.helper.pixels_to_meters( + self.latitude, self.zoom) / + (_TILESIZE / 2. / self.radius_meters)))) + + lon_pixels = _EARTHPIX + self.longitude * math.radians(_PIXRAD) + + sin_lat = math.sin(math.radians(self.latitude)) + lat_pixels = _EARTHPIX - _PIXRAD * math.log((1+sin_lat)/(1-sin_lat))/2 + self.big_size = self.num_tiles * _TILESIZE + big_image = self.helper.new_image(self.big_size, self.big_size) + + for j in range(self.num_tiles): + lon = self._pixels_to_lon(j, lon_pixels) + for k in range(self.num_tiles): + lat = self._pixels_to_lat(k, lat_pixels) + tile = self._grab_tile(lon, lat) + big_image.paste(tile, (j * _TILESIZE, k * _TILESIZE)) + + west = self._pixels_to_lon(0, lon_pixels) + east = self._pixels_to_lon(self.num_tiles - 1, lon_pixels) + + north = self._pixels_to_lat(0, lat_pixels) + south = self._pixels_to_lat(self.num_tiles - 1, lat_pixels) + return big_image, (north, west), (south, east) + + def move_pix(self, dx, dy): + """ + Function gets change in x and y (dx, dy) + then displaces the displayed map that amount + + NO RETURN + """ + self._constrain_x(dx) + self._constrain_y(dy) + self.update() + + def _constrain_x(self, diff): + """ + Helper for move_pix + """ + new_value = self.left_x - diff + + if !(new_value > 0 and + (new_value < self.big_image.size[0] - self.width)): + return self.left_x + else: + return new_value + + def _constrain_y(self, diff): + """ + Helper for move_pix + """ + new_value = self.upper_y - diff + + if !(new_value > 0 and + new_value < self.big_image.size[1] - self.height): + return self.upper_y + else: + return new_value + + def update(self): + """ + Function remakes display image using top left corners + """ + self.display_image.paste(self.big_image, (-self.left_x, -self.upper_y)) + # self.display_image.resize((self.image_zoom, self.image_zoom)) + + def _fetch(self): + """ + Function generates big image + """ + self.big_image, self.northwest, self.southeast = self.fetch_tiles() + + def move_latlon(self, lat, lon): + """ + Function to move the object/rover + """ + x, y = self._get_cartesian(lat, lon) + self._constrain_x(self.center_x-x) + self._constrain_y(self.center_y-y) + self.update() + + def _get_cartesian(self, lat, lon): + """ + Helper for getting the x, y given lat and lon + + returns INT, INT (x, y) + """ + viewport_lat_nw, viewport_lon_nw = self.northwest + viewport_lat_se, viewport_lon_se = self.southeast + # print "Lat:", viewport_lat_nw, viewport_lat_se + # print "Lon:", viewport_lon_nw, viewport_lon_se + + viewport_lat_diff = viewport_lat_nw - viewport_lat_se + viewport_lon_diff = viewport_lon_se - viewport_lon_nw + + # print viewport_lon_diff, viewport_lat_diff + + bigimage_width = self.big_image.size[0] + bigimage_height = self.big_image.size[1] + + pixel_per_lat = bigimage_height / viewport_lat_diff + pixel_per_lon = bigimage_width / viewport_lon_diff + # print "Pixel per:", pixel_per_lat, pixel_per_lon + + new_lat_gps_range_percentage = (viewport_lat_nw - lat) + new_lon_gps_range_percentage = (lon - viewport_lon_nw) + # print lon, viewport_lon_se + + x = new_lon_gps_range_percentage * pixel_per_lon + y = new_lat_gps_range_percentage * pixel_per_lat + + return int(x), int(y) + + def add_gps_location(self, lat, lon, shape, size, fill): + """ + Function adds a shape at lat x lon + """ + x, y = self._get_cartesian(lat, lon) + draw = PIL.ImageDraw.Draw(self.big_image) + if shape is "ellipsis": + draw.ellipsis((x-size, y-size, x+size, y+size), fill) + else: + draw.rectangle([x-size, y-size, x+size, y+size], fill) + self.update() + + def center_display(self, lat, lon): + """ + Function centers the display image + """ + x, y = self._get_cartesian(lat, lon) + self.center_x = x + self.center_y = y + + self.left_x = (self.center_x - (self.width/2)) + self.upper_y = (self.center_y - (self.height/2)) + self.update() + + # def update_rover_map_location(self, lat, lon): + # print "I did nothing" + + # def draw_circle(self, lat, lon, radius, fill): + # print "I did nothing" diff --git a/software/ground_station/Framework/MapSystems/RoverMapCoordinator.py b/software/ground_station/Framework/MapSystems/RoverMapCoordinator.py new file mode 100644 index 0000000..6a915ab --- /dev/null +++ b/software/ground_station/Framework/MapSystems/RoverMapCoordinator.py @@ -0,0 +1,60 @@ +##################################### +# Imports +##################################### +# Python native imports +from PyQt5 import QtCore, QtWidgets +import logging + +import rospy + +# Custom Imports +import RoverMap + +##################################### +# Global Variables +##################################### +# put some stuff here later so you can remember + + +class RoverMapCoordinator(QtCore.QThread): + pixmap_ready_signal = QtCore.pyqtSignal(str) + + def __init__(self, shared_objects): + super(RoverMapCoordinator, self).init() + + self.shared_objects = shared_objects + self.left_screen = self.shared_objects["screens"]["left_screen"] + self.mapping_label = self.left_screen.mapping_label + + self.setings = QtCore.QSettings() + + self.logger = logging.getLogger("groundstation") + + self.run_thread_flag = True + self.setup_map_flag = True + + # setup map + self._setup_map_threads() + + def run(self): + self.logger.debug("Starting Map Coordinator Thread") + + while self.run_thread_flag: + self.msleep(10) + + self.__wait_for_map_thread() + self.logger.debug("Stopping Map Coordinator Thread") + + def __wait_for_map_thread(self): + self.map_thread.wait() + + def _setup_map_threads(self): + self.map_thread = RoverMap.GMapsStitcher(1280, + 720, 44.567161, -123.278432, + 18, 'terrain', None, 20) + + def pixmap_ready_slot(self): + self.mapping_label.setPixmap(self.map_thread.display_image) + + def on_kill_threads_requested_slot(self): + self.run_thread_flag = False \ No newline at end of file diff --git a/software/ground_station/Framework/MapSystems/RoverMapHelper.py b/software/ground_station/Framework/MapSystems/RoverMapHelper.py new file mode 100644 index 0000000..759209b --- /dev/null +++ b/software/ground_station/Framework/MapSystems/RoverMapHelper.py @@ -0,0 +1,43 @@ +import PIL.Image +import math + + +class MapHelper(object): + + @staticmethod + def new_image(width, height): + """ + Generates a new image using PIL.Image module + + returns PIL.IMAGE OBJECT + """ + return PIL.Image.new('RGBA', (width, height)) + + @staticmethod + def fast_round(value, precision): + """ + Function to round values instead of using python's + + return INT + """ + return int(value * 10 ** precision) / 10. ** precision + + @staticmethod + def pixels_to_degrees(pixels, zoom): + """ + Generates pixels to be expected at zoom levels + + returns INT + """ + return pixels * 2 ** (21-zoom) + + @staticmethod + def pixels_to_meters(latitude, zoom): + """ + Function generates how many pixels per meter it + should be from the projecction + + returns FLOAT + """ + # https://groups.google.com/forum/#!topic/google-maps-js-api-v3/hDRO4oHVSeM + return 2 ** zoom / (156543.03392 * math.cos(math.radians(latitude))) diff --git a/software/ground_station/Framework/MapSystems/signing.py b/software/ground_station/Framework/MapSystems/signing.py new file mode 100644 index 0000000..d5f519a --- /dev/null +++ b/software/ground_station/Framework/MapSystems/signing.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" Signs a URL using a URL signing secret """ + +import hashlib +import hmac +import base64 +import urlparse + +def sign_url(input_url=None, secret=None): + """ Sign a request URL with a URL signing secret. + + Usage: + from urlsigner import sign_url + + signed_url = sign_url(input_url=my_url, secret=SECRET) + + Args: + input_url - The URL to sign + secret - Your URL signing secret + + Returns: + The signed request URL + """ + + if not input_url or not secret: + raise Exception("Both input_url and secret are required") + + url = urlparse.urlparse(input_url) + + # We only need to sign the path+query part of the string + url_to_sign = url.path + "?" + url.query + + # Decode the private key into its binary format + # We need to decode the URL-encoded private key + decoded_key = base64.urlsafe_b64decode(secret) + + # Create a signature using the private key and the URL-encoded + # string using HMAC SHA1. This signature will be binary. + signature = hmac.new(decoded_key, url_to_sign, hashlib.sha1) + + # Encode the binary signature into base64 for use within a URL + encoded_signature = base64.urlsafe_b64encode(signature.digest()) + + original_url = url.scheme + "://" + url.netloc + url.path + "?" + url.query + + # Return signed URL + return original_url + "&signature=" + encoded_signature + +if __name__ == "__main__": + input_url = raw_input("URL to Sign: ") + secret = raw_input("URL signing secret: ") + print "Signed URL: " + sign_url(input_url, secret) diff --git a/software/ground_station/Resources/Ui/left_screen.ui b/software/ground_station/Resources/Ui/left_screen.ui index bdafc73..c154798 100644 --- a/software/ground_station/Resources/Ui/left_screen.ui +++ b/software/ground_station/Resources/Ui/left_screen.ui @@ -119,7 +119,7 @@ color: #DCDCDC; 0 - + 0 diff --git a/software/ground_station/RoverGroundStation.py b/software/ground_station/RoverGroundStation.py index 74c2473..392ee5c 100755 --- a/software/ground_station/RoverGroundStation.py +++ b/software/ground_station/RoverGroundStation.py @@ -95,6 +95,7 @@ class GroundStation(QtCore.QObject): # ##### Instantiate Threaded Classes ###### self.__add_thread("Video Coordinator", RoverVideoCoordinator.RoverVideoCoordinator(self.shared_objects)) + self.__add_thread("Map Coordinator", RoverMapCoordinator.RoverMapCoordinator(self.shared_objects)) self.connect_signals_and_slots_signal.emit() self.__connect_signals_to_slots()