""" location.py: definition of CoreLocation class that is a member of the Session object. Provides conversions between Cartesian and geographic coordinate systems. Depends on utm contributed module, from https://pypi.python.org/pypi/utm (version 0.3.0). """ import logging from core.emulator.enumerations import RegisterTlvs from core.location import utm class CoreLocation: """ Member of session class for handling global location data. This keeps track of a latitude/longitude/altitude reference point and scale in order to convert between X,Y and geo coordinates. """ name = "location" config_type = RegisterTlvs.UTILITY.value def __init__(self): """ Creates a MobilityManager instance. :return: nothing """ # ConfigurableManager.__init__(self) self.reset() self.zonemap = {} self.refxyz = (0.0, 0.0, 0.0) self.refscale = 1.0 self.zoneshifts = {} self.refgeo = (0.0, 0.0, 0.0) for n, l in utm.ZONE_LETTERS: self.zonemap[l] = n def reset(self): """ Reset to initial state. """ # (x, y, z) coordinates of the point given by self.refgeo self.refxyz = (0.0, 0.0, 0.0) # decimal latitude, longitude, and altitude at the point (x, y, z) self.setrefgeo(0.0, 0.0, 0.0) # 100 pixels equals this many meters self.refscale = 1.0 # cached distance to refpt in other zones self.zoneshifts = {} def px2m(self, val): """ Convert the specified value in pixels to meters using the configured scale. The scale is given as s, where 100 pixels = s meters. :param val: value to use in converting to meters :return: value converted to meters """ return (val / 100.0) * self.refscale def m2px(self, val): """ Convert the specified value in meters to pixels using the configured scale. The scale is given as s, where 100 pixels = s meters. :param val: value to convert to pixels :return: value converted to pixels """ if self.refscale == 0.0: return 0.0 return 100.0 * (val / self.refscale) def setrefgeo(self, lat, lon, alt): """ Record the geographical reference point decimal (lat, lon, alt) and convert and store its UTM equivalent for later use. :param lat: latitude :param lon: longitude :param alt: altitude :return: nothing """ self.refgeo = (lat, lon, alt) # easting, northing, zone e, n, zonen, zonel = utm.from_latlon(lat, lon) self.refutm = ((zonen, zonel), e, n, alt) def getgeo(self, x, y, z): """ Given (x, y, z) Cartesian coordinates, convert them to latitude, longitude, and altitude based on the configured reference point and scale. :param x: x value :param y: y value :param z: z value :return: lat, lon, alt values for provided coordinates :rtype: tuple """ # shift (x,y,z) over to reference point (x,y,z) x -= self.refxyz[0] y = -(y - self.refxyz[1]) if z is None: z = self.refxyz[2] else: z -= self.refxyz[2] # use UTM coordinates since unit is meters zone = self.refutm[0] if zone == "": raise ValueError("reference point not configured") e = self.refutm[1] + self.px2m(x) n = self.refutm[2] + self.px2m(y) alt = self.refutm[3] + self.px2m(z) (e, n, zone) = self.getutmzoneshift(e, n) try: lat, lon = utm.to_latlon(e, n, zone[0], zone[1]) except utm.OutOfRangeError: logging.exception( "UTM out of range error for n=%s zone=%s xyz=(%s,%s,%s)", n, zone, x, y, z, ) lat, lon = self.refgeo[:2] return lat, lon, alt def getxyz(self, lat, lon, alt): """ Given latitude, longitude, and altitude location data, convert them to (x, y, z) Cartesian coordinates based on the configured reference point and scale. Lat/lon is converted to UTM meter coordinates, UTM zones are accounted for, and the scale turns meters to pixels. :param lat: latitude :param lon: longitude :param alt: altitude :return: converted x, y, z coordinates :rtype: tuple """ # convert lat/lon to UTM coordinates in meters e, n, zonen, zonel = utm.from_latlon(lat, lon) _rlat, _rlon, ralt = self.refgeo xshift = self.geteastingshift(zonen, zonel) if xshift is None: xm = e - self.refutm[1] else: xm = e + xshift yshift = self.getnorthingshift(zonen, zonel) if yshift is None: ym = n - self.refutm[2] else: ym = n + yshift zm = alt - ralt # shift (x,y,z) over to reference point (x,y,z) x = self.m2px(xm) + self.refxyz[0] y = -(self.m2px(ym) + self.refxyz[1]) z = self.m2px(zm) + self.refxyz[2] return x, y, z def geteastingshift(self, zonen, zonel): """ If the lat, lon coordinates being converted are located in a different UTM zone than the canvas reference point, the UTM meters may need to be shifted. This picks a reference point in the same longitudinal band (UTM zone number) as the provided zone, to calculate the shift in meters for the x coordinate. :param zonen: zonen :param zonel: zone1 :return: the x shift value """ rzonen = int(self.refutm[0][0]) # same zone number, no x shift required if zonen == rzonen: return None z = (zonen, zonel) # x shift already calculated, cached if z in self.zoneshifts and self.zoneshifts[z][0] is not None: return self.zoneshifts[z][0] rlat, rlon, _ralt = self.refgeo # ea. zone is 6deg band lon2 = rlon + 6 * (zonen - rzonen) # ignore northing e2, _n2, _zonen2, _zonel2 = utm.from_latlon(rlat, lon2) # NOTE: great circle distance used here, not reference ellipsoid! xshift = utm.haversine(rlon, rlat, lon2, rlat) - e2 # cache the return value yshift = None if z in self.zoneshifts: yshift = self.zoneshifts[z][1] self.zoneshifts[z] = (xshift, yshift) return xshift def getnorthingshift(self, zonen, zonel): """ If the lat, lon coordinates being converted are located in a different UTM zone than the canvas reference point, the UTM meters may need to be shifted. This picks a reference point in the same latitude band (UTM zone letter) as the provided zone, to calculate the shift in meters for the y coordinate. :param zonen: zonen :param zonel: zone1 :return: calculated y shift """ rzonel = self.refutm[0][1] # same zone letter, no y shift required if zonel == rzonel: return None z = (zonen, zonel) # y shift already calculated, cached if z in self.zoneshifts and self.zoneshifts[z][1] is not None: return self.zoneshifts[z][1] rlat, rlon, _ralt = self.refgeo # zonemap is used to calculate degrees difference between zone letters latshift = self.zonemap[zonel] - self.zonemap[rzonel] # ea. latitude band is 8deg high lat2 = rlat + latshift _e2, n2, _zonen2, _zonel2 = utm.from_latlon(lat2, rlon) # NOTE: great circle distance used here, not reference ellipsoid yshift = -(utm.haversine(rlon, rlat, rlon, lat2) + n2) # cache the return value xshift = None if z in self.zoneshifts: xshift = self.zoneshifts[z][0] self.zoneshifts[z] = (xshift, yshift) return yshift def getutmzoneshift(self, e, n): """ Given UTM easting and northing values, check if they fall outside the reference point's zone boundary. Return the UTM coordinates in a different zone and the new zone if they do. Zone lettering is only changed when the reference point is in the opposite hemisphere. :param e: easting value :param n: northing value :return: modified easting, northing, and zone values :rtype: tuple """ zone = self.refutm[0] rlat, rlon, _ralt = self.refgeo if e > 834000 or e < 166000: num_zones = (int(e) - 166000) / (utm.R / 10) # estimate number of zones to shift, E (positive) or W (negative) rlon2 = self.refgeo[1] + (num_zones * 6) _e2, _n2, zonen2, zonel2 = utm.from_latlon(rlat, rlon2) xshift = utm.haversine(rlon, rlat, rlon2, rlat) # after >3 zones away from refpt, the above estimate won't work # (the above estimate could be improved) if not 100000 <= (e - xshift) < 1000000: # move one more zone away num_zones = (abs(num_zones) + 1) * (abs(num_zones) / num_zones) rlon2 = self.refgeo[1] + (num_zones * 6) _e2, _n2, zonen2, zonel2 = utm.from_latlon(rlat, rlon2) xshift = utm.haversine(rlon, rlat, rlon2, rlat) e = e - xshift zone = (zonen2, zonel2) if n < 0: # refpt in northern hemisphere and we crossed south of equator n += 10000000 zone = (zone[0], "M") elif n > 10000000: # refpt in southern hemisphere and we crossed north of equator n -= 10000000 zone = (zone[0], "N") return e, n, zone