def equirectangular_move(latitude, longitude, bearing, distance, compass_bearing=True): ''' move on hypothetical equirectangular earth a purely latitudinal move should be same on all 3 models >>> equirectangular_move(60.0, -110.0, 0, DEGREE_IN_METERS) (61.0, -110.0) a longitudinal move will only equal latitudinal in this model >>> equirectangular_move(60.0, -110.0, -90, DEGREE_IN_METERS) (60.0, -111.0) ''' if compass_bearing: # expressed as clockwise degrees from due north radians = math.radians(cartesian(bearing)) else: radians = math.radians(bearing) dx = math.sin(radians) * (distance / DEGREE_IN_METERS) dy = math.cos(radians) * (distance / DEGREE_IN_METERS) logging.debug('dx %s, dy %s, new latitude %s, new longitude %s', dx, dy, latitude + dx, longitude + dy) return (latitude + dx, longitude + dy)
def correct_for_no_data(elevations, raw, farthest): ''' for any "no data" (-32768) points in the list, average known elevations modifies list "in place" >>> test = [[13], [-32768], [-32768]] >>> correct_for_no_data(test, 0, [0]) >>> test [[13], [9], [4]] >>> test = [[13], [-32768], [-32768], [-32768], [26]] >>> correct_for_no_data(test, 0, [0]) >>> test [[13], [16], [20], [23], [26]] ''' last_known = farthest[raw] state, count, increment = 'scanning', 0, 0 for index in range(len(elevations) - 1, -1, -1): elevation = elevations[index][raw] logging.debug('state: %s, count: %d', state, count) if elevation == -32768: # no data for this point logging.debug('found "no data" point at index %d', index) if state == 'scanning': state = 'counting' count = 1 else: count += 1 else: logging.debug('found valid data at index %d', index) if state == 'scanning': last_known = elevation else: increment = float(last_known - elevation) / (count + 1) logging.debug('correcting with increment %.2f', increment) for i in range(count + 1): elevations[index + i][raw] = int( round(elevation + (i * increment))) logging.debug('elevation[%d] is now %s', index + i, elevations[index + i]) last_known = elevation state = 'scanning'
def spherical_move(latitude, longitude, bearing, distance, compass_bearing=True): ''' resulting latitude and longitude from as-the-bullet-flies move from http://gis.stackexchange.com/a/30037/1291, but corrected for the Cartesian sense of zero degrees that the Python math module uses. a purely latitudinal move should be same on all 3 models >>> spherical_move(60.0, -110.0, 0, DEGREE_IN_METERS) (61.0, -110.0) a longitudinal move will cover more than 1 degree north or south of the equator, twice as much at the 60th parallel at equator, latitude is returned as a very small positive quantity so we round it >>> tuple(map(round, spherical_move(0.0, -110.0, -90, DEGREE_IN_METERS))) (0.0, -111.0) >>> spherical_move(60.0, -110.0, -90, DEGREE_IN_METERS) (60.0, -112.0) ''' if compass_bearing: # expressed as clockwise degrees from due north radians = math.radians(cartesian(bearing)) else: radians = math.radians(bearing) distance_x = math.cos(radians) * distance distance_y = math.sin(radians) * distance logging.debug('distances to move: (%.3fx, %.3fy)', distance_x, distance_y) degrees_y = distance_y / DEGREE_IN_METERS average_latitude = math.radians(latitude + (degrees_y / 2)) degrees_x = (distance_x / math.cos(average_latitude)) / DEGREE_IN_METERS return (degrees_y + latitude, degrees_x + longitude)
def relative_bearing(bearing, longitude, compass_bearing=True): ''' correct bearing for azimuthal equidistant "flat earth" it is relative to the longitude angle, which always points "north" return as cartesian angle in radians, *not* compass >>> round(relative_bearing(0, 0), 2) -1.57 >>> round(relative_bearing(0, 180), 2) 1.57 >>> round(relative_bearing(0, 90), 2) 3.14 >>> round(relative_bearing(10, 180), 2) 1.4 ''' if not compass_bearing: bearing = compass(bearing) logging.debug('bearing %s, longitude %s', bearing, longitude) bearing = cartesian(180 + longitude + bearing) logging.debug('relative bearing: %s', bearing) return math.radians(bearing)
def azimuthal_equidistant_move(latitude, longitude, bearing, distance, compass_bearing=True): ''' move on hypothetical flat earth there must be a way to do this without going back and forth from Cartesian system, but math is hard and this will have to do for now. a purely latitudinal move should be same on all 3 models but "purely latitudinal" on this model can only be from the bottom of the "disk", longitude 180 >>> azimuthal_equidistant_move(60.0, 180.0, 0, DEGREE_IN_METERS) (61.0, 180.0) a move westward at 60 degrees north should cover about 2 degrees, and due to circular latitude "lines" should be south of starting point >>> azimuthal_equidistant_move(0.0, 180.0, -90, DEGREE_IN_METERS) (-0.005555384098371974, -179.36340642403653) >>> azimuthal_equidistant_move(60.0, 180.0, -90, DEGREE_IN_METERS) (59.98333796039273, -178.0908475670036) ''' logging.debug('move: %s, %s, %s, %s, %s', latitude, longitude, bearing, distance, compass_bearing) radians = relative_bearing(bearing, longitude, compass_bearing) rho, theta = latitude_to_rho(latitude), math.radians(longitude) x = rho * math.sin(theta) y = rho * math.cos(theta) logging.debug('before: x %s, y %s, rho %s, theta %s', x, y, rho, theta) logging.debug('check on theta %s: %s', theta, math.atan2(y, x)) dx, dy = equirectangular_move(0, 0, bearing, distance, False) x, y = x + dx, y + dy rho = math.sqrt((x * x) + (y * y)) theta = math.atan2(y, x) logging.debug('after: x %s, y %s, rho %s, theta %s', x, y, rho, theta) return (rho_to_latitude(rho), compass(math.degrees(theta)))
def panorama(bearing, latitude, longitude, distance=500, height=CAMERA_HEIGHT, span=SPAN): ''' display view of horizon from a point at given bearing height is meters from ground to eye level. maximum distance in km can be shorter to speed processing. bearing is counterclockwise angle starting at due east. span is the number of degrees to view. pixel height is determined by angle of elevation as seen from viewer's position, which varies inversely with distance. pixels represent a uniform distance at `distance`, so the determined angle should be divided by delta(bearing) (`d_bearing`) to get pixels in height. ''' step = SAMPLE_IN_METERS # meters to "move" while tracing out horizon logging.info('bearing (compass): %.2f', bearing) bearing = math.radians(cartesian(bearing)) logging.info('bearing (cartesian): %.2f', math.degrees(bearing)) viewrange = distance * 1000 # km to meters logging.info('radius: %s, step: %s', RADIUS, step) height += get_height(latitude, longitude) logging.info('initial height: %s', height) halfspan = math.radians(span) / 2 d_bearing = math.asin(float(step) / viewrange) logging.info('delta angle: %s (%s)', d_bearing, math.degrees(d_bearing)) angle = bearing + halfspan logging.info('initial angle: %s', math.degrees(angle)) logging.info('final angle: %s', math.degrees(bearing - halfspan)) # elevations will be expressed in 3 ways: # 0. actual elevation above sea level; # 1. 'y' pixel coordinate after correcting for perspective # (also for curvature if enabled in model) # 2. 'y' as mapped to PIL.Image coordinates (closer, current, farther) = (raw, y_cartesian, y_image) = (0, 1, 2) elevations = [] image_height = 360 horizon = int(image_height / 2) # initializers for bad pixels: nearest = [0, -horizon, image_height - 1] farthest = [0, 0, horizon - 1] while angle > bearing - halfspan: elevations.append([[e] + nearest[y_cartesian:] for e in look( math.degrees(angle), latitude, longitude, distance, step)]) correct_for_no_data(elevations[-1], raw, farthest) for index in range(1, len(elevations[-1])): elevation = elevations[-1][index][raw] # factor in curvature if enabled elevation -= earthcurvature(step * index, 'm', 'm')[1][2] # apparent elevation is reduced by eye height above sea level elevation -= height theta = math.atan(elevation / (step * index)) # now convert radians to projected pixels projected = int(round(theta / abs(d_bearing))) elevations[-1][index][y_cartesian] = projected elevations[-1][index][y_image] = horizon - 1 - projected logging.debug('angle: %s, elevations: %s', angle, elevations[-1]) angle -= d_bearing width = len(elevations) logging.info('width of image: %d', width) # initialize to sky blue panorama = Image.new('RGBA', (width, image_height), (128, 128, 255, 255)) for index in range(width): x = index # adding a previous level of `image_height` will ensure a ridge line # gets drawn on the most distant plot # adding the spot on which the observer is standing will allow # checking the arc of view of each point pointlist = elevations[index] pointlist = [pointlist[0]] + pointlist + [[None, None, image_height]] logging.debug('index: %d, pointlist: %s', index, pointlist) color = WHITEPIXEL for depth in range(len(pointlist) - 2, 0, -1): context = pointlist[depth - 1:depth + 2] logging.debug('context: %s', context) if context[current][y_image] == context[closer][y_image]: # carry forward the previous "farther" value logging.debug('carrying over previous value at depth %d', depth) pointlist[depth] = pointlist[depth + 1] elif context[current][y_cartesian] > context[closer][y_cartesian]: if OCEANFRONT and context[current][raw] == 0: logging.debug('ocean at depth %d', depth) ridgecolor = color = BLUEPIXEL else: # farthest away will be shown lightest # map depth values from DARKEST to WHITE divider = len(pointlist) / float(WHITE - DARKEST) gray = int(depth / divider) + DARKEST color = (gray, gray, gray, OPAQUE) ridgecolor = BLACKPIXEL logging.debug('color at depth %d is %s', depth, color) # remember that (0, 0) is top left of PIL.Image y = max(0, context[current][y_image]) # mark the top of every ridge if y < context[farther][y_image]: logging.debug('marking top of ridge at (%s, %s)', x, y) putpixel(panorama, (x, y), ridgecolor) # don't overwrite black pixel from previous ridgeline elif (y > context[farther][y_image] or getpixel(panorama, (x, y)) != ridgecolor): logging.debug('not top of ridge at (%s, %s)', x, y) putpixel(panorama, (x, y), color) logging.debug('painting %s from %d to %d', color, y + 1, context[closer][y_image] + 1) for plot in range(y + 1, context[closer][y_image] + 1): putpixel(panorama, (x, plot), color) panorama.show()
from PIL import Image logging.basicConfig(level=logging.DEBUG if __debug__ else logging.INFO) SPAN = float(os.getenv('SPAN', '60.0')) logging.info('SPAN: %.02f', SPAN) WHITE = OPAQUE = COMPLETELY = 255 DARKEST = 10 BLACK = NONE = 0 BLACKPIXEL = (BLACK, BLACK, BLACK, OPAQUE) WHITEPIXEL = (WHITE, WHITE, WHITE, OPAQUE) BLUEPIXEL = (NONE, NONE, COMPLETELY, OPAQUE) OCEANFRONT = bool(os.getenv('OCEANFRONT')) CAMERA_HEIGHT = float(os.getenv('CAMERA_HEIGHT', '1.538')) FLAT_EARTH_RADIUS = math.pi * RADIUS # north pole to "ice rim" DEGREE_IN_METERS = (RADIUS * 2 * math.pi) / 360.0 SAMPLE_IN_METERS = DEGREE_IN_METERS / (60 * (60 / SAMPLE_SECONDS)) logging.debug('RADIUS: %s, DEGREE_IN_METERS: %s', RADIUS, DEGREE_IN_METERS) def putpixel(image, point, color): ''' plot a pixel on the image, ignoring anything outside image range ''' try: image.putpixel(point, color) except IndexError: pass def getpixel(image, point): ''' get a pixel from the image, ignoring anything outside image range