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
-
-
- 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()