Checkpoint.

debian-sid
Tom Payne 15 years ago
parent 6d88d757d1
commit a77d99e8ee

@ -0,0 +1,27 @@
class Bounds:
def __init__(self, min, max=None):
self.min = min
self.max = max or min
def merge(self, value):
if isinstance(value, Bounds):
if value.min < self.min:
self.min = value.min
if value.max > self.max:
self.max = value.max
else:
if value < self.min:
self.min = value
if value > self.max:
self.max = value
class BoundsSet:
def merge(self, other):
for key, value in other.__dict__.items():
if hasattr(self, key):
getattr(self, key).merge(value)
else:
setattr(self, key, Bounds(value.min, value.max))

@ -0,0 +1,72 @@
def rgb_to_kml(rgb):
return 'ff%02x%02x%02x' % (255 * rgb[2], 255 * rgb[1], 255 * rgb[0])
def _h_to_value(p, q, t):
if t < 0.0:
t += 1.0
elif 1.0 < t:
t -= 1.0
if t < 1.0 / 6.0:
return p + 6.0 * (q - p) * t
elif t < 0.5:
return q
elif t < 2.0 / 3.0:
return p + 6.0 * (q - p) * (2.0 / 3.0 - t)
else:
return p
def hsl_to_rgb(hsl):
"Convert a HSL tuple into a RGB tuple."
h, s, l = hsl
if s == 0:
return (l, l, l)
q = l * (s + 1.0) if l < 0.5 else l + s - l * s
p = 2.0 * l - q
r = _h_to_value(p, q, h + 1.0 / 3.0)
g = _h_to_value(p, q, h)
b = _h_to_value(p, q, h - 1.0 / 3.0)
return (r, g, b)
def hsv_to_rgb(hsv):
"Convert a HSV tuple into a ABGR tuple."
h, s, v = hsv
hi = int(h)
f = h - hi
p = v * (1.0 - f)
q = v * (1.0 - f * s)
t = v * (1.0 - (1.0 - f) * s)
if hi == 0:
return (v, t, p)
elif hi == 1:
return (q, v, p)
elif hi == 2:
return (p, v, t)
elif hi == 3:
return (p, q, v)
elif hi == 4:
return (t, p, v)
else:
return (v, p, q)
def grayscale(value):
"Return a gradient from black to white."
if value < 0.0:
return 'ff000000'
elif 1.0 <= value:
return 'ffffffff'
else:
return 'ff%02x%02x%02x' % (255 * value, 255 * value, 255 * value)
def default(value):
"Return a gradient from blue to green to red."
if value < 0.0:
return rgb_to_kml(hsl_to_rgb((2.0 / 3.0, 1.0, 0.5)))
elif 1.0 <= value:
return rgb_to_kml(hsl_to_rgb((0.0, 1.0, 0.5)))
else:
h = 2.0 * (1.0 - value) / 3.0
return rgb_to_kml(hsl_to_rgb((h, 1.0, 0.5)))

@ -1,3 +1,5 @@
from __future__ import with_statement
import datetime
import re
@ -136,14 +138,16 @@ PARSERS = {
class IGC:
def __init__(self, input):
def __init__(self, filename):
global PARSERS
self.filename = filename
self.c = []
self.g = []
self.h = {}
self.i = None
ignore = lambda l, s: None
self.records = [PARSERS.get(line[0], ignore)(line, self) for line in input]
with open(filename) as file:
self.records = [PARSERS.get(line[0], ignore)(line, self) for line in file]
def track(self):
coords = TimeSeries()
@ -158,6 +162,7 @@ class IGC:
t.append((record.dt - t0).seconds)
coords.t = t
meta = OpenStruct()
meta.name = self.filename
if self.h.has_key('plt') and self.h['plt'].strip() != 'not set':
meta.pilot_name = self.h['plt'].strip()
else:

@ -3,13 +3,55 @@
import fileinput
import optparse
import sys
import yaml
from bounds import Bounds, BoundsSet
import gradient
import igc
from track import Hints, Track
import kmz
from OpenStruct import OpenStruct
import scale
from track import Hints, Stock
def main():
track = igc.IGC(fileinput.input()).track()
track.kmz(Hints()).write('t.kmz')
def add_track(option, opt_str, value, parser, make_track, **kwargs):
track = make_track(value)
hints = Hints()
for attr in 'color glider_type pilot_name timezone_offset'.split(' '):
if not getattr(parser.values, attr) is None:
setattr(hints, attr, getattr(parser.values, attr))
setattr(parser.values, attr, None)
if parser.values.tracks_and_hints is None:
parser.values.tracks_and_hints = []
parser.values.tracks_and_hints.append((track, hints))
def main(argv):
parser = optparse.OptionParser(usage='Usage: %prog [options]')
parser.add_option('-o', '--output', dest='filename', metavar='FILENAME')
group = optparse.OptionGroup(parser, 'Per-track options')
group.add_option('-c', '--color', dest='color', metavar='COLOR')
group.add_option('-i', '--igc', action='callback', callback=add_track, dest='tracks_and_hints', metavar='FILENAME', nargs=1, type='string', callback_args=(lambda value: igc.IGC(value).track(),))
group.add_option('-g', '--glider-type', dest='glider_type', metavar='STRING')
group.add_option('-p', '--pilot-name', dest='pilot_name', metavar='STRING')
group.add_option('-w', '--width', dest='width', metavar='INTEGER', type='int')
group.add_option('-z', '--timezone-offset', dest='timezone_offset', metavar='INTEGER', type='int')
parser.add_option_group(group)
parser.set_defaults(output='igc2kmz.kmz')
options, args = parser.parse_args(argv)
if options.tracks_and_hints is None:
raise RuntimeError # FIXME
if len(args) != 1:
raise RuntimeError # FIXME
bounds = BoundsSet()
for track, hints in options.tracks_and_hints:
bounds.merge(track.bounds)
stock = Stock()
stock.altitude_scale = scale.Scale('altitude', (bounds.ele.min, bounds.ele.max), gradient.default)
result = kmz.kmz()
result.add_siblings(stock.kmz)
for track, hints in options.tracks_and_hints:
hints.stock = stock
result.add_siblings(track.kmz(hints))
result.write(options.output)
if __name__ == '__main__':
main()
main(sys.argv)

@ -3,27 +3,34 @@ import datetime
class Element:
"KML element base class."
def name(self):
"Return name."
return self.__class__.__name__
def id(self):
"Return a unique id."
return '%x' % id(self)
def url(self):
"Return a URL referring to self."
return '#%s' % self.id()
def write(self, file):
"Write self to file."
file.write(str(self))
class SimpleElement(Element):
"A KML element with no children."
def __init__(self, text=None, **kwargs):
self.text = None if text is None else str(text)
self.attrs = kwargs
def __str__(self):
"Return the KML representation of self."
attrs = '' if len(self.attrs) == 0 else ''.join([' %s="%s"' % pair for pair in self.attrs.items()])
if self.text is None:
return '<%s%s/>' % (self.name(), attrs)
@ -32,6 +39,7 @@ class SimpleElement(Element):
class CompoundElement(Element):
"A KML element with children."
def __init__(self, *args, **kwargs):
self.attrs = {}
@ -39,14 +47,17 @@ class CompoundElement(Element):
self.add(*args, **kwargs)
def add_attrs(self, **kwargs):
"Add attributes."
self.attrs.update(kwargs)
def add(self, *args, **kwargs):
"Add children."
self.children.extend(list(args))
for key, value in kwargs.items():
self.children.append(globals()[key](value))
def write(self, file):
"Write self to file."
attrs = '' if len(self.attrs) == 0 else ''.join([' %s="%s"' % pair for pair in self.attrs.items()])
if len(self.children) == 0:
file.write('<%s%s/>' % (self.name(), attrs))
@ -57,6 +68,7 @@ class CompoundElement(Element):
file.write('</%s>' % self.name())
def __str__(self):
"Return the KML representation of self."
attrs = '' if len(self.attrs) == 0 else ''.join([' %s="%s"' % pair for pair in self.attrs.items()])
if len(self.children) == 0:
return '<%s%s/>' % (self.name(), attrs)
@ -65,20 +77,24 @@ class CompoundElement(Element):
class CDATA:
"A KML CDATA."
def __init__(self, value):
self.value = value
def __str__(self):
"Return the KML representation of self."
return '<![CDATA[%s]]>' % self.value
class dateTime:
"A KML dateTime."
def __init__(self, value):
self.value = value
def __str__(self):
"Return the KML representation of self."
return self.value.strftime('%Y-%m-%dT%H:%M:%SZ')
@ -108,6 +124,7 @@ class kml(CompoundElement):
self.add_attrs(xmlns='http://earth.google.com/kml/%s' % version)
def write(self, file):
"Write self to file."
file.write('<?xml version="1.0" encoding="UTF-8"?>')
CompoundElement.write(self, file)

@ -52,7 +52,3 @@ class kmz:
for key, value in self.files.items():
zipfile.writestr(ZipInfo(key), value)
zipfile.close()
if __name__ == '__main__':
k = kmz(roots=kml.name('Tom'), files={'tom.txt': 'hello'})
k.write('tom.zip')

@ -0,0 +1,56 @@
class Scale:
"A linear scale."
def __init__(self, title, range, gradient):
self.title = title
self.range = map(float, range)
self.gradient = gradient
def normalize(self, value):
"Normalize value."
if value < self.range[0]:
return 0.0
elif self.range[1] <= value:
return 1.0
else:
return (value - self.range[0]) / (self.range[1] - self.range[0])
def discretize(self, value, n=32):
"Discretize value."
if value < self.range[0]:
return 0
elif value > self.range[1]:
return n - 1
else:
result = int(round(n * self.normalize(value)))
if result > n - 1:
return n - 1
else:
return result
def color(self, value):
"Return the color for value."
return self.gradient(self.normalize(value))
def colors(self, n=32):
"Return the colors."
return [self.gradient(float(i) / (n - 1)) for i in range(0, n)]
class ZeroCenteredScale(Scale):
"A bilinear scale centered on zero."
def normalize(self, value):
"Normalize value."
if value < 0.0:
if value < self.range[0]:
return 0.0
else:
return 0.5 - 0.5 * value / self.range[0]
elif value == 0.0:
return 0.5
else:
if self.range[1] <= value:
return 1.0
else:
return 0.5 + 0.5 * value / self.range[1]

@ -5,6 +5,7 @@ import sys
import cairo
import numpy
from bounds import Bounds, BoundsSet
import kml
import kmz
from OpenStruct import OpenStruct
@ -19,129 +20,6 @@ def runs(list):
i = j
yield((i, len(list)))
def h_to_value(p, q, t):
if t < 0.0:
t += 1.0
elif 1.0 < t:
t -= 1.0
if t < 1.0 / 6.0:
return p + 6.0 * (q - p) * t
elif t < 0.5:
return q
elif t < 2.0 / 3.0:
return p + 6.0 * (q - p) * (2.0 / 3.0 - t)
else:
return p
def hsl_to_rgb(hsl):
h, s, l = hsl
if s == 0:
return (l, l, l)
q = l * (s + 1.0) if l < 0.5 else l + s - l * s
p = 2.0 * l - q
r = h_to_value(p, q, h + 1.0 / 3.0)
g = h_to_value(p, q, h)
b = h_to_value(p, q, h - 1.0 / 3.0)
return (r, g, b)
def hsv_to_rgb(hsv):
h, s, v = hsv
hi = int(h)
f = h - hi
p = v * (1.0 - f)
q = v * (1.0 - f * s)
t = v * (1.0 - (1.0 - f) * s)
if hi == 0:
return (v, t, p)
elif hi == 1:
return (q, v, p)
elif hi == 2:
return (p, v, t)
elif hi == 3:
return (p, q, v)
elif hi == 4:
return (t, p, v)
else:
return (v, p, q)
def rgb_to_kml(rgb):
return 'ff%02x%02x%02x' % (255 * rgb[2], 255 * rgb[1], 255 * rgb[0])
def grayscaleGradient(value):
if value < 0.0:
return (0.0, 0.0, 0.0)
elif 1.0 <= value:
return (1.0, 1.0, 1.0)
else:
return (value, value, value)
def defaultGradient(value):
if value < 0.0:
return hsl_to_rgb((2.0 / 3.0, 1.0, 0.5))
elif 1.0 <= value:
return hsl_to_rgb((0.0, 1.0, 0.5))
else:
h = 2.0 * (1.0 - value) / 3.0
return hsl_to_rgb((h, 1.0, 0.5))
class Scale:
def __init__(self, title, range, gradient):
self.title = title
self.range = map(float, range)
self.gradient = gradient
def normalize(self, value):
if value < self.range[0]:
return 0.0
elif self.range[1] <= value:
return 1.0
else:
return (value - self.range[0]) / (self.range[1] - self.range[0])
def discretize(self, value, n=32):
if value < self.range[0]:
return 0
elif value > self.range[1]:
return n - 1
else:
result = int(round(n * self.normalize(value)))
if result > n - 1:
return n - 1
else:
return result
def rgb(self, value):
return self.gradient(self.normalize(value))
def rgbs(self, n=32):
return [self.gradient(float(i) / (n - 1)) for i in range(0, n)]
class ZeroCenteredScale(Scale):
def normalize(self, value):
if value < 0.0:
if value < self.range[0]:
return 0.0
else:
return 0.5 - 0.5 * value / self.range[0]
elif value == 0.0:
return 0.5
else:
if self.range[1] <= value:
return 1.0
else:
return 0.5 + 0.5 * value / self.range[1]
class Stock:
def make_pixel(self):
@ -180,37 +58,23 @@ class Hints:
def __init__(self):
self.altitude_mode = 'absolute'
self.color = 'ff0000ff'
self.stock = Stock()
self.width = 2
class Bounds:
def __init__(self, value):
self.min = self.max = value
def update(self, value):
if value < self.min:
self.min = value
if self.max < value:
self.max = value
class Track:
def __init__(self, meta, times, coords):
self.meta = meta
self.times = times
self.coords = coords
self.analyse()
def analyse(self, period=20):
half_period = period / 2.0
self.bounds = OpenStruct()
self.bounds = BoundsSet()
self.bounds.ele = Bounds(self.coords[0].ele)
for coord in self.coords:
self.bounds.ele.update(coord.ele)
self.bounds.ele.merge(coord.ele)
self.elevation_data = self.bounds.ele.min != 0 or self.bounds.ele.max != 0
def analyse(self, period=20):
half_period = period / 2.0
self.dz_positive = [0]
self.s = [0]
for i in range(1, len(self.coords)):
@ -220,59 +84,32 @@ class Track:
x = math.sin(math.pi * self.coords[i - 1].lat / 180.0) * math.sin(math.pi * self.coords[i].lat / 180.0) + math.cos(math.pi * self.coords[i - 1].lat / 180.0) * math.cos(math.pi * self.coords[i].lat) * math.cos(math.pi * (self.coords[i - 1].lon - self.coords[i].lon) / 180.0)
ds = 6371.0 * math.acos(x) if x < 1.0 else 0.0
self.s.append(self.s[i - 1] + ds)
"""
n = len(self.coords)
rlat = numpy.empty((n + 2,))
rlon = numpy.empty((n + 2,))
ele = numpy.empty((n + 2,))
ds = numpy.empty((n + 2,))
t = numpy.empty((n + 2,))
rlat[0] = math.pi * self.coords[0].lat / 180.0
rlon[0] = math.pi * self.coords[0].lon / 180.0
ele[0] = self.coords[0].ele
ds[0] = 0.0
t[0] = -sys.maxint - 1
for i in range(0, n):
rlat[i + 1] = math.pi * self.coords[i].lat / 180.0
rlon[i + 1] = math.pi * self.coords[i].lon / 180.0
ele[i + 1] = self.coords[i].ele
x = sin(rlat[i]) * sin(rlat[i + 1]) + cos(rlat[i]) * cos(rlat[i + 1]) * cos(rlon[i] - rlon[i - 1])
if x < 1.0:
ds[i + 1] = 6371000.0 * acos(x)
else:
ds[i + 1] = 0.0
rlat[-1] = math.pi * self.coords[-1].lat / 180.0
rlon[-1] = math.pi * self.coords[-1].lon / 180.0
ele[-1] = self.coords[-1].ele
ds[-1] = 0.0
t[-1] = sys.maxint
"""
def make_description(self, hints):
rows = []
if not self.meta.pilot_name is None:
rows.append(('Pilot name', self.meta.pilot_name))
if not self.meta.glider_type is None:
rows.append(('Glider type', self.meta.glider_type))
if not self.meta.glider_id is None:
rows.append(('Glider ID', self.meta.glider_id))
rows.append(('Take-off time', self.times[0].strftime('%H:%M:%S')))
rows.append(('Landing time', self.times[-1].strftime('%H:%M:%S')))
hour, seconds = divmod((self.times[0] - self.times[-1]).seconds, 3600)
minute, second = divmod(seconds, 60)
rows.append(('Duration', '%d:%02d:%02d' % (hour, minute, second)))
print self.s[-1]
rows.append(('Track length', '%.3fkm' % (self.s[-1] / 1000.0)))
if self.elevation_data:
rows.append(('Take-off altitude', '%dm' % self.coords[0].ele))
rows.append(('Maximum altitude', '%dm' % self.bounds.ele.max))
rows.append(('Accumulated altitude gain', '%dm' % self.dz_positive[-1]))
rows.append(('Landing altitude', '%dm' % self.coords[-1].ele))
description = kml.description(kml.CDATA('<table>%s</table>' % ''.join(['<tr><th align="right">%s</th><td>%s</td></tr>' % row for row in rows])))
snippet = kml.Snippet(self.meta.pilot_name) # FIXME
return kmz.kmz(description, snippet)
if 0:
n = len(self.coords)
rlat = numpy.empty((n + 2,))
rlon = numpy.empty((n + 2,))
ele = numpy.empty((n + 2,))
ds = numpy.empty((n + 2,))
t = numpy.empty((n + 2,))
rlat[0] = math.pi * self.coords[0].lat / 180.0
rlon[0] = math.pi * self.coords[0].lon / 180.0
ele[0] = self.coords[0].ele
ds[0] = 0.0
t[0] = -sys.maxint - 1
for i in range(0, n):
rlat[i + 1] = math.pi * self.coords[i].lat / 180.0
rlon[i + 1] = math.pi * self.coords[i].lon / 180.0
ele[i + 1] = self.coords[i].ele
x = sin(rlat[i]) * sin(rlat[i + 1]) + cos(rlat[i]) * cos(rlat[i + 1]) * cos(rlon[i] - rlon[i - 1])
if x < 1.0:
ds[i + 1] = 6371000.0 * acos(x)
else:
ds[i + 1] = 0.0
rlat[-1] = math.pi * self.coords[-1].lat / 180.0
rlon[-1] = math.pi * self.coords[-1].lon / 180.0
ele[-1] = self.coords[-1].ele
ds[-1] = 0.0
t[-1] = sys.maxint
def make_solid_track(self, hints, style, altitude_mode, extrude=None, **folder_options):
line_string = kml.LineString(coordinates=self.coords, altitudeMode=altitude_mode)
@ -284,7 +121,7 @@ class Track:
def make_colored_track(self, hints, values, scale, altitude_mode, **folder_options):
folder = kml.Folder(name='Colored by %s' % scale.title, styleUrl=hints.stock.check_hide_children_style.url(), **folder_options)
styles = [kml.Style(kml.LineStyle(color=rgb_to_kml(rgb), width=hints.width)) for rgb in scale.rgbs()]
styles = [kml.Style(kml.LineStyle(color=color, width=hints.width)) for color in scale.colors()]
discrete_values = map(scale.discretize, values)
for start, end in runs(discrete_values):
line_string = kml.LineString(coordinates=self.coords[start:end + 1], altitudeMode=hints.altitude_mode)
@ -297,7 +134,7 @@ class Track:
folder = kmz.kmz(kml.Folder(name='Track', open=1, styleUrl=hints.stock.radio_folder_style.url()))
folder.add(hints.stock.invisible_none_folder)
if self.elevation_data:
folder.add(self.make_colored_track(hints, map(lambda c: c.ele, self.coords), hints.altitude_scale, 'absolute'))
folder.add(self.make_colored_track(hints, map(lambda c: c.ele, self.coords), hints.stock.altitude_scale, 'absolute'))
folder.add(self.make_solid_track(hints, kml.Style(kml.LineStyle(color=hints.color, width=hints.width)), hints.altitude_mode, name='Solid color', visibility=0))
return folder
@ -312,11 +149,27 @@ class Track:
return folder
def kmz(self, hints):
result = kmz.kmz()
hints.altitude_scale = Scale('altitude', (self.bounds.ele.min, self.bounds.ele.max), defaultGradient)
result.add_siblings(hints.stock.kmz)
result.add_siblings(self.make_description(hints))
result.add_siblings(open=1)
result.add_siblings(self.make_track_folder(hints))
result.add_siblings(self.make_shadow_folder(hints))
return result
folder = kmz.kmz(kml.Folder(name=self.meta.name, open=1))
rows = []
if not self.meta.pilot_name is None:
rows.append(('Pilot name', self.meta.pilot_name))
if not self.meta.glider_type is None:
rows.append(('Glider type', self.meta.glider_type))
if not self.meta.glider_id is None:
rows.append(('Glider ID', self.meta.glider_id))
rows.append(('Take-off time', self.times[0].strftime('%H:%M:%S')))
rows.append(('Landing time', self.times[-1].strftime('%H:%M:%S')))
hour, seconds = divmod((self.times[0] - self.times[-1]).seconds, 3600)
minute, second = divmod(seconds, 60)
rows.append(('Duration', '%d:%02d:%02d' % (hour, minute, second)))
#rows.append(('Track length', '%.3fkm' % (self.s[-1] / 1000.0)))
if self.elevation_data:
rows.append(('Take-off altitude', '%dm' % self.coords[0].ele))
rows.append(('Maximum altitude', '%dm' % self.bounds.ele.max))
#rows.append(('Accumulated altitude gain', '%dm' % self.dz_positive[-1]))
rows.append(('Landing altitude', '%dm' % self.coords[-1].ele))
folder.add(kml.description(kml.CDATA('<table>%s</table>' % ''.join(['<tr><th align="right">%s</th><td>%s</td></tr>' % row for row in rows]))))
folder.add(kml.Snippet(self.meta.pilot_name)) # FIXME
folder.add(self.make_track_folder(hints))
folder.add(self.make_shadow_folder(hints))
return folder

Loading…
Cancel
Save