-- draft default tip
authorhh
Mon, 18 May 2020 08:48:51 +0200
changeset 0 bb616224c02a
--
kml/__init__.py
kml/config.py
kml/flyby.py
kml/hh_http.py
kml/kml.py
kml/kmlcgi.py
kml/targets.py
loxodroma.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kml/config.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,246 @@
+import json
+import os
+
+import targets
+from kml import *
+
+
+class Config:
+    class Value:
+        def __init__(self, key, val):
+            self.key = key
+            self.val = val
+            self.chk = True
+            self.keyact = key + '_actual'
+
+        def extract(self, p):
+            if self.key in p:
+                self.val = p[self.key][0]
+            self.chk = True
+
+    class ValueN(Value):
+        def extract(self, p):
+            if self.key in p:
+                try:
+                    self.val = float(p[self.key][0])
+                    self.chk = True
+                except:
+                    self.val = p[self.key][0]
+                    self.chk = False
+
+    def __init__(self):
+        # -------------------------------------------------
+        # zadání - vstupní veličiny
+        # -------------------------------------------------
+        self.name = self.Value('NA', 'flyby')
+        self.targname = self.Value('TA', '')
+        self.lat = self.ValueN('LA', targets.Stalin.lat)
+        self.long = self.ValueN('LO', targets.Stalin.long)
+        self.h0 = self.ValueN('H0', 300)
+        self.x0 = self.ValueN('X0', 8000)
+        self.y0 = self.ValueN('Y0', -12000)
+        self.z0 = self.ValueN('Z0', 12000)
+        self.x1 = self.ValueN('X1', 0)
+        self.y1 = self.ValueN('Y1', -150)
+        self.z1 = self.ValueN('Z1', 100)
+        self.exp = self.ValueN('EX', 2)
+        self.steps = self.ValueN('ST', 600)
+        self.speedfact = self.ValueN('SPF', 1)
+        self.slowdown = self.Value('SLD', 'tangential')
+        self.slowdfact = self.ValueN('SLF', 1)
+        self.flyby = self.ValueN('FL', 1)
+        self.influentials = ['name', 'targname', 'lat', 'long', 'h0', 'x0', 'y0', 'z0',
+                       'x1', 'y1', 'z1', 'exp', 'steps', 'speedfact', 'slowdown', 'slowdfact', 'flyby']
+
+        # -------------------------------------------------
+        # počáteční podmínky
+        # -------------------------------------------------
+        self.dXmin = 0.1            # min delta X při numerické konvergenci
+        self.minZ = 60				# minimální přípustná výška nad terénem v metrech
+        self.dHmax = 5				# empirické maximální přípustné otočení pohledu v jednom kroku ve stupních
+        self.durOpt = 23			# empirické optimum: rychlost otáčení pohledu = 23°/sec
+        self.stepsMax = 9999
+
+        # -------------------------------------------------
+        # provozní konstanty
+        # -------------------------------------------------
+        self.configsrv = 'kml'
+        self.dnldsrv = 'flyby'
+        if 'HTTP_HOST' in os.environ:
+            h = os.environ['HTTP_HOST'].split('.')
+            self.srvhost = h[0].lower()
+            self.srvdomain = '.'.join(h[1:])
+            self.dnldqual = '.'.join((self.dnldsrv, self.srvdomain))
+            self.configqual = '.'.join((self.configsrv, self.srvdomain))
+            _reqPath = os.environ['HTTP_HOST'] + os.environ['REQUEST_URI'].split('?')[0]
+            _configKeyWord = 'kml'
+            _dnldKeyWord = 'flyby'
+            self.configReq = _reqPath.lower().find(_configKeyWord) > -1
+            self.dnldReq = _reqPath.lower().find(_dnldKeyWord) > -1
+            _reqUrl = '{scheme}://{host}:{port}{path}'.\
+                format(scheme=os.environ['REQUEST_SCHEME'],
+                       host=os.environ['HTTP_HOST'],
+                       port=os.environ['SERVER_PORT'],
+                       path=os.environ['REQUEST_URI'].split('?')[0])
+            self.dnldUrl = _reqUrl.lower().replace(_configKeyWord, _dnldKeyWord)
+            deb('self.dnldUrl=%s' % self.dnldUrl)
+            self.dnldPath = _reqPath.lower().replace(_configKeyWord, _dnldKeyWord)
+            deb('self.dnldPath=%s' % self.dnldPath)
+        self.defaultlat = 50.1
+        self.defaultlong = 14.3
+        self.parmsdir = 'params'
+        self.targsdir = 'targets'
+        self.kmlfn = 'flyby'		# jméno výstupu - neměnné
+        self.zipped = False
+        # zipping se nedá v současnosti používat, protože Google Earth se s ním nechová standardně
+        # - viz .../memo/googleearth
+        self.kmlfile = '{}.{}'.format(self.kmlfn, 'kmz' if self.zipped else 'kml')
+        self.urlfile = 'url.kml'
+        self.msg = ''
+        self.flagged = False
+
+    def adjust(self):
+        self.x0.val = fabs(self.x0.val)
+        if self.x0.val < self.dXmin:
+            self.x0.val = 0
+        self.x1.val = 0
+        if self.z0.val < self.minZ:
+            self.z0.val = self.minZ
+        if self.z1.val < self.minZ:
+            self.z1.val = self.minZ
+        self.steps.val = int(round(self.steps.val))
+        if self.steps.val <= 0:
+            self.steps.val = 20
+        if self.steps.val > self.stepsMax:
+            self.steps.val = self.stepsMax
+        self.exp.val = fabs(self.exp.val)
+        self.flyby.val = int(fabs(self.flyby.val))
+
+        if self.x0.val > 0:
+            self.r0 = degrees(atan(self.y0.val / self.x0.val))				# lokální azimut na startu
+            self.r1 = 90 if self.y1.val < 0 else -90						# lokální azimut v průletu
+            if self.y1.val == 0:
+                if self.y0.val == 0:
+                    self.r1 = 90
+                else:
+                    self.r1 = 90 * fabs(self.y0.val) / self.y0.val
+                    if self.exp.val <= 1:
+                        self.r1 = -self.r1
+            self.h1 = can360(self.h0.val + self.r0 + self.r1)				# azimut v průletu
+            self.h2 = can360(self.h0.val + 2 * (self.h1 - self.h0.val))		# azimut na konci
+
+        dY = self.y1.val - self.y0.val
+        dZ = self.z1.val - self.z0.val
+        if dY == 0:
+            self.A = pi / 2 if dZ < 0 else -pi / 2
+        else:
+            self.A = atan(dZ / dY)											# sklon roviny letu, horiz = 0
+            if dY > 0:
+                self.A = self.A + pi if dZ <= 0 else self.A - pi
+
+        F = sqrt(pow(dY, 2) + pow(dZ, 2))									# maximální funkční hodnota
+        if self.x0.val > 0:
+            self.K = F / pow(self.x0.val, self.exp.val)		# koeficient paraboly
+
+        sa = SpeedAdjust(self.speedfact.val, self.slowdfact.val, self.steps.val, self)
+        self.speedAdjust = sa.slowdowns[self.slowdown.val](sa)
+
+        self.lastH = self.h0.val
+        self.lastX, self.lastY, self.lastZ = self.x0.val, self.y0.val, self.z0.val
+        self.dX = -2 * self.x0.val / self.steps.val
+        if fabs(self.dX) < self.dXmin:
+            self.dX = -self.dXmin
+        self.sumSteps, self.sumDur = 0, 0
+        self.rr = []
+
+    def update_influentials(self, p):
+        for n in self.influentials:
+            v = eval('self.{}'.format(n))
+            v.extract(p)
+            self.flagged += (not v.chk)
+
+        v = self.lat
+        if v.chk:
+            v.chk = (v.val >= -90 and v.val <= 90)
+        v = self.long
+        if v.chk:
+            v.chk = v.val >= -180 and v.val <= 180
+        v = self.slowdown
+        v.chk = v.val in SpeedAdjust.keys
+
+        self.flagged = False
+        for n in self.influentials:
+            v = eval('self.{}'.format(n))
+            self.flagged += (not v.chk)
+
+    def parmspath(self):
+        return os.path.join(self.parmsdir, self.name.val)
+
+    def parmsls(self):
+        l = os.listdir(self.parmsdir)
+        l.sort()
+        #deb("l={}".format(l))
+        return l
+
+    def targspath(self, name):
+        return os.path.join(self.targsdir, name)
+
+    def targsls(self):
+        l = os.listdir(self.targsdir)
+        l.sort()
+        return l
+
+    def gettarget(self, name):
+        fn = name if name != '' else 'point'
+        path = self.targspath(fn)
+        long = self.defaultlong
+        lat = self.defaultlat
+        if os.path.exists(path):
+            g = {}
+            for p in open(path).read().split(','):
+                k, v = p.split('=')
+                g[k] = v
+            if self.long.key in g:
+                long = g[self.long.key]
+            if self.lat.key in g:
+                lat = g[self.lat.key]
+        return long, lat
+
+    def storetarget(self, name, long, lat):
+        fn = name if name != '' else 'point'
+        path = self.targspath(fn)
+        open(path, mode='w').write('{}={},{}={}'.format(self.lat.key, lat, self.long.key, long))
+
+    def restoreparms(self):
+        if not os.path.exists(self.parmspath()):
+            return
+        parms = dict(json.load(open(self.parmspath())))
+        # for p in open(self.parmspath()).read().split(','):
+        #     k,v = p.split('=')
+        #     parms[k] = [v]
+        self.update_influentials(parms)
+
+    def storeparms(self):
+        if not self.name:
+            return
+        # s = []
+        p = {}
+        for n in self.influentials:
+            v = eval('self.{}'.format(n))
+            # s = s + ['='.join((v.key,str(v.val)))]
+            p[v.key] = [str(v.val)]
+        # open(self.parmspath(), mode='w').write(','.join(s))
+        json.dump(p, open(self.parmspath(), mode='w'), ensure_ascii=False)
+
+    def printparms(self):
+        s = []
+        for n in self.influentials:
+            v = eval('self.{}'.format(n))
+            s = s + ['{}={}'.format(v.key, v.val)]
+        return '\n'.join(s)
+
+    def adjustenv(self):
+        p = {}
+        for k in os.environ:
+            p[k] = [os.environ[k]]
+        return p
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kml/flyby.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,243 @@
+# coding=utf8
+
+import targets
+import config
+from kml import *
+import sys
+import os
+import time
+from math import *
+
+"""
+# -----------------------------------------------------------------------------------------------------------
+# pokusy s KML v Googleearth
+#   ● program generuje položky LookAt tak, aby vznikl pohyb kamery jako symetrický průlet kolem cílového bodu,
+#		na který kamera po celou dobu letu hledí
+#   ● cílový bod leží vždy v počátku lokálních souřadnic letu
+#   ● zadávané souřadnice výchozího bodu a průletu jsou lokální souřadnice (v metrech) vzhledem k cílovému bodu
+#   ● výchozí bod je X0>=0, Y0, Z0>minZ>0; pro X0=0 se vytváří jen jediný LookAt
+#   ● bod průletu je X1=0, Y1, Z1>minZ>0
+#   ● křivka průletu je symetrická podle roviny Y,Z a je to buď parabola nebo 2 polopřímky s počátkem v průletu
+#   ● pro Z0=Z1 probíhá let ve vodorovné rovině, pro Y0=Y1 ve svislé rovině rovnoběžné s X
+#   ● zadávaný exponent paraboly exp>0; pro exp=1 přejde parabola do polopřímek
+#   ● zadávané geodetické souřadnice cílového bodu a azimut kamery na startu uchycují lokální soustavu do geodetických souřadnic,
+#		výška cílového bodu je vždy 0 nad terénem
+#   ● v modulu kml jsou  dispozici geo-souřadnice několika konkrétních bodů
+#   ● zadává se počet kroků (LookAt), které se generují; propočítávají se body z 1.poloviny letu,
+#		body 2.poloviny jsou symetrické (mají symetrický lokální azimut)
+#   ● doba přeletu v každém kroku se určuje podle empirických pravidel:
+#		1. podle výšky kamery: rychlost v m/s je číselně rovna střední výšce v m
+#		2. podle úhlu otočení v kroku: rychlost taková, aby otáčení vycházelo cca 20°/s
+#		** volí se nižší hodnota rychlosti z obou pravidel
+#   ● průběh rychlosti se může zadat jako nelineární se zpomalením v průletu;
+#		- k dispozici je 5 podtříd SpeedAdjust od konstatní po exponenciální
+#		- každá třída je parametrizována váhovými koeficienty pro konstatntní a proměnnou složku rychlosti
+#   ● Googleearth při změnách azimutu mezi dvěma LookAt většími než empirických dHmax nepohybuje kamerou lineárně,
+#		proto funkce compStep hlídá změnu azimutu v kroku (dH) a přizpůsobuje dX tak, aby dH byl těsně menší než dHmax
+#   ● pro každý krok se vypočítává lokální azimut "polohy" kamery (Ri), z něhož se určuje geo-azimut "pohledu" kamery
+#		(lokální azimut 0 leží na kladné poloose X)
+#   ● lokální azimut v průletu je R1=+/-90°, pro Y1=0 (kamera hledí svisle dolů) se pokládá R1=-90° pro exp>1 jinak 90°
+#   ● když chci obrátit směr pohybu kamery po dráze, obrátím znaménka Y0 a Y1
+# 		a výchozí azimut H0 změním na konečný azimut H2; vypočtené hodnoty se vypisují v komentáři na začátku výstupu
+#   ● zadáním switche "průlet" se řídí vytvoření celého průletu nebo jen letu do největšího přiblížení (bodu průletu)
+#   ● výstup ve tvaru Tour v KML Documentu jde na sys.stdout
+#
+# formáty GEO-souřadnic:
+# 	50.1009147N, 14.4057506E		mapy.cz
+# 	50.765779°   15.050950°			google earth
+# 	50°02'44.9"N 14°25'52.4"E		google maps
+# 	50.045796, 14.431211
+# 	36°58′41″N 82°34′37″W			wikipedia
+# -----------------------------------------------------------------------------------------------------------
+"""
+class Flyby():
+    k = Kml()
+
+    def __init__(self, config):
+        self.c = config
+
+    def pr(self, s):
+        print(s, file=self.k.out)
+
+    def compMove(self):
+        c = self.c
+        xi = c.xi
+        Ri = 0
+        if (c.x0.val > 0 and xi == c.x0.val) or c.x0.val == 0:
+            yi, zi = c.y0.val, c.z0.val
+            heading = c.h0.val
+        elif(xi == 0):
+            yi, zi = c.y1.val, c.z1.val
+            heading = c.h1
+        else:
+            try: f = c.K * pow(xi, c.exp.val)
+            except:
+                deb('ABEND: K={}, xi={}, exp={}'.format(c.K, xi, c.exp.val))
+                sys.stderr.close()
+                os._exit(1)
+            yi = c.y1.val + f * cos(c.A)
+            zi = c.z1.val + f * sin(c.A)
+            Ri = degrees(atan(yi / xi))
+            heading = can360(c.h0.val + c.r0 - Ri)
+
+        dist = sqrt(pow(xi,2) + pow(yi,2) + pow(zi,2))
+        tilt = degrees(acos(zi / dist))
+
+        dX = xi - c.lastX
+        dY = yi - c.lastY
+        dZ = zi - c.lastZ
+        dS = sqrt(pow(dX,2) + pow(dY,2) + pow(dZ,2))
+        tDur = dS / pow((zi + fabs(dZ/2)), 1)					# empirické optimum: rychlost letu = výška/sec
+        dH = can180(heading - c.lastH)
+        rDur = fabs(dH) / c.durOpt								# empirické optimum: rychlost otáčení
+        rawdur = rDur if rDur > tDur else tDur
+        dur = rawdur * c.speedAdjust.koef() if c.x0.val > 0 else rawdur
+
+        # self.drspeed = dS/rawdur if rawdur > 0 else -0.0		# +++
+        # self.dspeed = dS/dur if dur > 0 else -0.0				# +++
+        # self.dkoef = c.speedAdjust.koef()						# +++
+        # self.drdur = rDur										# +++
+        # self.dtdur = tDur										# +++
+
+        c.yi, c.zi = yi, zi
+
+        s = ('Xi={:.3f}, dX={:.3f}, Yi={:.1f}, dY={:.3f}, Zi={:.1f}, dZ={:.3f}, dS={:.3f}, Ri={:.3f}, dist={:.1f}, tilt={:.1f}, '
+             .format(xi, dX, yi, dY, zi, dZ, dS, Ri, dist, tilt),
+            'heading={:.3f}, dH={:.3f}, dur={:.3f}'.format(heading, dH, dur))
+
+        return (s, dur, dist, tilt, heading)
+
+    def compStep(self):
+        c = self.c
+        (s, dur, dist, tilt, heading) = self.compMove()
+        dH = can180(heading - c.lastH)
+        bad = fabs(dH) > c.dHmax
+        if(bad and fabs(c.dX) > c.dXmin):
+            ddX = -c.dX/2
+            last = bad
+            found = 0
+            while(fabs(dH) > 0 and fabs(dH) != c.dHmax and fabs(ddX) > c.dXmin):
+                c.dX = c.dX + ddX
+                c.xi = c.lastX + c.dX
+                (s, dur, dist, tilt, heading) = self.compMove()
+                dH = can180(heading - c.lastH)
+                bad = fabs(dH) > c.dHmax
+                if(not bad): found = c.xi
+                ddX = ddX/2 if bad == last else -ddX/2
+                last = bad
+            if(fabs(c.dX) > c.xi): c.dX = -c.xi
+            if(bad and found > 0):
+                c.xi = found
+                s, dur, dist, tilt, heading = self.compMove()
+                dH = can180(heading - c.lastH)
+        # deb('Xi={:.3f}, i={:.3f}, koef = {:.3f}, speed={:.3f}, adj speed={:.3f}, trans dur={:.3f}, rot dur={:.3f}'
+        # 	.format(c.xi, 1 - c.xi / c.x0.val, self.dkoef, self.drspeed, self.dspeed, self.dtdur, self.drdur))		# +++
+        return s, dur, dist, tilt, heading, dH
+
+    def compTour(self):
+        c = self.c
+
+        c.xi = c.x0.val
+        s, dur, dist, tilt, heading, dH = self.compStep()
+        self.fly(s[0]+s[1], 5, False, dist, tilt, heading)
+        if(c.x0.val == 0): return
+
+        c.xi = c.xi + c.dX
+        while(c.xi > fabs(c.dX)):
+            s, dur, dist, tilt, heading, dH = self.compStep()
+            self.fly(s[0]+s[1], dur, True, dist, tilt, heading)
+            c.xi = c.xi + c.dX
+
+        self.k.comm('>>> střed')
+        c.xi = 0
+        s, dur, dist, tilt, heading, dH = self.compStep()
+        if(fabs(dH) <= c.dHmax): self.fly(s[0]+s[1], dur, True, dist, tilt, heading)
+        while(c.xi > c.dXmin):
+            self.k.comm('>>> dotažení středu')
+            c.xi = 0
+            s, dur, dist, tilt, heading, dH = self.compStep()
+            if(fabs(dH) <= c.dHmax): self.fly(s[0]+s[1], dur, True, dist, tilt, heading)
+            else: break
+        if(fabs(dH) > c.dHmax):
+            self.k.comm('>>> dotočení středu')
+            c.xi = 0
+            s, dur, dist, tilt, heading = self.compMove()
+            d = c.dHmax * dH / fabs(dH)
+            h = c.lastH
+            while(fabs(dH) > c.dHmax):
+                h = can360(h + d)
+                dur = fabs(d)/c.durOpt
+                s_ = s[0] + 'heading={:.3f}, dH={:.3f}, dur={:.3f}'.format(h, d, dur)
+                self.fly(s_, dur, True, dist, tilt, h)
+                dH = can180(heading - h)
+            if(fabs(dH) > 0):
+                dur = fabs(dH)/c.durOpt
+                s_ = s[0] + 'heading={:.3f}, dH={:.3f}, dur={:.3f}'.format(heading, dH, dur)
+                self.fly(s_, dur, True, dist, tilt, heading)
+
+        if(c.flyby):
+            c = self.c
+            del c.rr[0]			# středový záznam se škrtá, neopakuje se
+            lastH = c.h1
+            for i in c.rr:
+                nDur, dist, tilt, heading = i
+                heading = can360(2 * c.h1 - heading)			# heading je zrcadlový vůči svislé rovině headingu v průletu (H1)
+                dH = can180(heading - lastH)
+                s_ = 'dist={:.1f}, tilt={:.1f}, heading={:.3f}, dH={:.3f}, dur={:.3f}'.format(dist, tilt, heading, dH, dur)
+                self.fly(s_, dur, True, dist, tilt, heading)	# duration se bere z předchozího záznamu
+                dur = nDur
+                lastH = heading
+
+        self.k.comm('sum: steps={}, duration={:.2f}'.format(c.sumSteps, c.sumDur))
+
+    def step(self, dur, dist, tilt, heading):
+        c = self.c
+        c.lastH = heading
+        c.lastX, c.lastY, c.lastZ = c.xi, c.yi, c.zi
+        c.rr = [(dur, dist, tilt, heading)] + c.rr
+        c.sumDur = c.sumDur + dur
+        c.sumSteps = c.sumSteps + 1
+
+    def fly(self, s, dur, smooth, dist, tilt, heading):
+        self.step(dur, dist, tilt, heading)
+        self.k.comm(s)
+        self.k.flyto.head(dur, smooth)
+        self.k.lookat.run(self.c.long.val, self.c.lat.val, 0, heading, tilt, dist)
+        # self.k.camera.runL(c.long.val, c.lat.val, 0, heading, tilt, dist, 0)
+        self.k.flyto.tail()
+
+    def headcomms(self):
+        c = self.c
+        self.k.comm(time.asctime())
+        c.confcomm = ('lat={lat}, long={long}, heading={heading}, exp={exp}, ' +
+                    'flyby={flyby}, speedfactor={speedf}, slowdown={slowd}, slowdfactor={slowdf}, steps={steps}')\
+                .format(target=c.targname.val, long=c.long.val, lat=c.lat.val, heading=c.h0.val, exp=c.exp.val,
+                    flyby=c.flyby.val, speedf=c.speedfact.val, slowd=c.slowdown.val, slowdf=c.slowdfact.val, steps=c.steps.val)
+
+        if(c.x0.val == 0):
+            c.coorcomm = 'X0,Y0,Z0={},{},{}, H0={}'.format(c.x0.val, c.y0.val, c.z0.val, c.h0.val)
+        else:
+            c.coorcomm = ('X0,Y0,Z0={},{},{}, X1,Y1,Z1={},{},{}, '.format(c.x0.val, c.y0.val, c.z0.val, c.x1.val, c.y1.val, c.z1.val) +
+                        'A={:.3f}, H0={}, H1={:.3f}, H2={:.3f}, R0={:.3f}, R1={:.3f}'
+                        .format(degrees(c.A), c.h0.val, c.h1, c.h2, c.r0, c.r1))
+
+        self.k.comm(c.confcomm)
+        self.k.comm(c.coorcomm)
+
+    def tour(self):
+        fkml = '{}.kml'.format(int(time.time())) if self.c.zipped else self.c.kmlfile
+        self.k.out = open(fkml, mode='w', encoding='utf-8')
+        self.k.xml.head()
+        self.k.tourProlog(self.c.name.val)
+        self.headcomms()
+        self.compTour()
+        self.k.tourEpilog()
+        self.k.xml.tail()
+        self.k.out.close()
+        if self.c.zipped:
+            fkmz = '{}.kmz'.format(self.c.kmlfn)
+            if os.path.exists(fkmz):
+                os.remove(fkmz)
+            from subprocess import Popen
+            p = Popen(['zip', fkmz, fkml], stdout=sys.stderr)
+            p.wait()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kml/hh_http.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,258 @@
+# coding=utf8
+
+import sys
+import os
+import time
+import cgi
+import cgitb
+import html
+from kml import *
+from kml import deb
+from subprocess import Popen
+
+class Http():
+    titles = {}
+    titles['general'] = """
+● vytvoří KML-stream z elementů LookAt pro GoogleEarth tak, aby vznikl pohyb kamery jako symetrický průlet kolem cílového bodu, na který kamera po celou dobu letu hledí
+● cílový bod leží v počátku lokálních souřadnic letu
+● křivka průletu je parabola y = abs(Y1 + x**exp) s počátkem v bodu průletu a je symetrická podle roviny y,z lokálních souřadnic
+● zadávané souřadnice výchozího bodu a bodu průletu jsou lokální souřadnice (v metrech) vzhledem k cílovému bodu
+● výchozí bod má souřadnice X0>=0, Y0, Z0>{minZ}; pro X0=0 se vytváří jen jediný LookAt (vlastně Placemark)
+● bod průletu má souřadnice X1=0, Y1, Z1>{minZ}
+● zadávaný exponent paraboly je exp>0; pro exp=1 přejde parabola do polopřímek
+● pro Z0=Z1 probíhá let ve vodorovné rovině, pro Y0=Y1 ve svislé rovině rovnoběžné s osou x
+● zadávané geodetické souřadnice cílového bodu (ve stupních) a azimut kamery na startu (sever=0) uchycují lokální soustavu do geodetických souřadnic, výška cílového bodu je vždy 0 nad terénem
+● průběh rychlosti se může zadat jako nelineární se zpomalením v průletu s několika druhy průběhu
+● přepínač "flyby" řídí vytvoření celého průletu nebo jen letu do největšího přiblížení (do bodu průletu)
+● konfigurace letů a geo-souřadnice cílových bodů je možno pojmenovat a uložit ("store conf" resp. "store geo") a později vytáhnout do formuláře ("get conf" resp. "get geo")
+"""
+    titles['name'] = 'jméno pro uložení aktuální konfigurace z obrazovky na server {server}'
+    titles['target'] = 'jméno pro uložení aktuálních geodetických souřadnic cílového bodu z obrazovky na server {server}'
+    titles['submitstored'] = 'vytvořit KML-stream z konfigurace uložené na serveru {server} pod zadaným jménem'
+    titles['submitconf'] = 'vytvořit KML-stream z konfigurace vyplněné ve formuláři na obrazovce'
+    titles['storeconf'] = 'uložit konfiguraci na server {server} pod jménem zadaným v položce name'
+    titles['getconf'] = 'vytáhnout konfiguraci uloženou na serveru {server} pod zadaným jménem'
+    titles['storegeo'] = 'uložit souřadnice cílového bodu na server {server} pod jménem zadaným v položce target'
+    titles['getgeo'] = 'vytáhnout souřadnice cílového bodu uložené na serveru {server} pod jménem zadaným v položce target'
+    titles['azimuth'] = 'geodetický azimut cílového bodu při pohledu z výchozího bodu x0,y0,z0'
+    titles['speedfact'] = 'koeficient konstantní složky rychlosti letu >0'
+    titles['slowfact'] = 'koeficient zpomalení v průletu >0'
+    titles['slowdown'] = 'průběh zpomalení v průletu'
+    titles['exponent'] = 'exponent (>0) průletové paraboly x = abs(Y1 + x**exp); pro exp=1 přejde parabola do polopřímek s počátkem v bodu průletu'
+    titles['flyby'] = 'přepínač průlet/nálet; nálet = let z výchozího bodu do maximálního přiblížení (bodu průletu)'
+    titles['steps'] = 'počet bodů KML-streamu < {stepsMax}'
+    titles['xyz0'] = 'lokální souřadnice X0>=0, Y0, Z0>{minZ} v metrech výchozího (krajního) bodu průletové paraboly x = abs(Y1 + x**exp); počátek lokálních souřadnic je v cílovém bodě'
+    titles['xyz1'] = 'lokální souřadnice X1=0, Y1, Z1>{minZ} bodu průletu kolem cílového bodu po parabole x = abs(Y1 + x**exp); počátek lokálních souřadnic je v cílovém bodě'
+    titles['lat'] = 'geodetická šířka cílového bodu ve stupních -90<š<90; cílový bod je zároveň počátkem lokálních souřadnic letové paraboly'
+    titles['long'] = 'geodetická délka cílového bodu ve stupních -180<d<180; cílový bod je zároveň počátkem lokálních souřadnic letové paraboly'
+
+    def __init__(self, config):
+        self.c = config
+        self.titles['general'] = self.titles['general'].format(minZ=self.c.minZ)
+        self.titles['xyz0'] = self.titles['xyz0'].format(minZ=self.c.minZ)
+        self.titles['xyz1'] = self.titles['xyz1'].format(minZ=self.c.minZ)
+        self.titles['steps'] = self.titles['steps'].format(stepsMax=self.c.stepsMax)
+        self.titles['name'] = self.titles['name'].format(server=self.c.configqual)
+        self.titles['target'] = self.titles['target'].format(server=self.c.configqual)
+        self.titles['submitstored'] = self.titles['submitstored'].format(server=self.c.configqual)
+        self.titles['storeconf'] = self.titles['storeconf'].format(server=self.c.configqual)
+        self.titles['getconf'] = self.titles['getconf'].format(server=self.c.configqual)
+        self.titles['storegeo'] = self.titles['storegeo'].format(server=self.c.configqual)
+        self.titles['getgeo'] = self.titles['getgeo'].format(server=self.c.configqual)
+        # deb(self.titles)
+
+    def parsehttp(self, msg):
+        c = self.c
+        # cgitb.enable()
+        p = cgi.parse()
+        # deb(('parsehttp, p', p))
+
+        c.domainname = '.'.join(os.environ['HTTP_HOST'].split('.')[1:])
+        if c.name.keyact in p: c.name.val = p[c.name.keyact][0]
+        if c.name.key in p: c.name.val = p[c.name.key][0]
+        if c.targname.keyact in p: c.targname.val = p[c.targname.keyact][0]
+        if c.slowdown.keyact in p: c.slowdown.val = p[c.slowdown.keyact][0]
+
+        if 'downlkml' in p:
+            self.sendfile(c.kmlfile)
+            return False
+        if 'downlurl' in p:
+            self.httpheader(c.urlfile)
+            Kml().urlPlacemark(c.dnldUrl, c.dnldPath)
+            sys.stdout.close()
+            # self.sendfile(c.urlfile)
+            return False
+
+        c.restoreparms()
+
+        if 'getparms' in p:
+            return False
+        if 'submitstored' in p:
+            if c.flagged:
+                return False
+            else:
+                c.adjust()
+                return True
+
+        c.update_influentials(p)
+
+        if 'gettarget' in p:
+            c.long.val, c.lat.val = c.gettarget(c.targname.val)
+            return False
+        if 'storetarget' in p and c.lat.chk and c.long.chk:
+            c.storetarget(c.targname.val, c.long.val, c.lat.val)
+            return False
+
+        if c.flagged:
+            return False
+        c.adjust()
+        if 'submit' in p:
+            return True
+        if 'storeparms' in p:
+            c.storeparms()
+            return False
+        return False
+
+    def htmlhead(self):
+        print("Content-type: text/html")
+        print('')
+        print('<!DOCTYPE html><html><head>')
+        print('<meta charset=\"utf-8\"/>')
+        print('<style type="text/css">')
+        print('.box {border: 1px solid; padding: .4em}')
+        print('.round {border:.12em solid; border-radius:50%; width:1.2em; line-height:1.2em; text-align:center;}')
+        print('.flag {background-color: #ffaaaa}')
+        print('.emph {color: #ff00ff; font-weight: bold}')
+        print('</style>')
+        print('</head><body>')
+        print('<div style="display:flex" title="{}">'.format(html.escape(self.titles['general'])))
+        print('<br><span class="box"><b>Google Earth parabolic flyby</b></span>')
+        print('&nbsp;<div><div class="round"><b>?</b></div></div>')
+        print('</div><br>')
+
+    def htmltrail(self):
+        print('</body></html>')
+
+    def sendform(self, confirm=True):
+        c = self.c
+        # cgitb.enable()
+        # cgi.test()
+        self.htmlhead()
+        if(confirm): self.sendconfirm()
+
+        if(c.flagged):
+            print('<p style="color: red"><b>FLAG</b></p>')
+        print('<form method=POST>')
+        print('<input type=hidden name="{kaname}" value="{actualname}">'
+              .format(kaname=c.name.keyact, actualname=c.name.val))
+        print('<input type=hidden name="{katargname}" value="{actualtargname}">'
+              .format(katargname=c.targname.keyact, actualtargname=c.targname.val))
+        print('<input type=hidden name="{kaslowdown}" value="{actualsldn}">'
+              .format(kaslowdown=c.slowdown.keyact, actualsldn=c.slowdown.val))
+
+        print('<table><tr><td><label><b>name</b> [{name}]<br><input type="text" name="{kname}" list="names" title="{tname}">'
+              .format(kname=c.name.key, name=c.name.val, tname=self.titles['name']))
+        print('<datalist id="names">')
+        for i in c.parmsls():
+            print('<option value="{}"/>'.format(i))
+        print('</datalist></input></label></td></tr></table>')
+        print('<input type=submit name="getparms" value="get stored conf" title="{}">'
+              .format(self.titles['getconf']))
+        print('<input type=submit name="storeparms" value="store conf" title="{}">'
+              .format(self.titles['storeconf']))
+        print('<input type=submit name="submitstored" value="submit stored conf" title="{}">'
+              .format(html.escape(self.titles['submitstored'])))
+        print('<br><br>')
+
+        print('<table><tr><td><label><b>target</b> [{targname}]<br><input type="text" name="{ktargname}" list="targets" title="{ttarget}">'
+              .format(ktargname=c.targname.key, targname=c.targname.val, ttarget=self.titles['target']))
+        print('<datalist id="targets">')
+        for i in c.targsls():
+            print('<option value="{}"/>'.format(i))
+        print('</datalist></input></label></td>')
+        print('<td><label><b>latitude<br><input type="text" name="{kla}" value="{vla}" title="{lat}" class="{flag}"></td>'
+              .format(kla=c.lat.key, vla=c.lat.val, lat=self.titles['lat'], flag='' if c.lat.chk else 'flag'))
+        print('<td><label><b>longitude<br><input type="text" name="{klo}" value="{vlo}" title="{long}" class="{flag}"></td>'
+              .format(klo=c.long.key, vlo=c.long.val, long=self.titles['long'], flag='' if c.long.chk else 'flag'))
+        print('</tr></table>')
+        print('<input type=submit name="gettarget" value="get stored geo" title="{}">'
+              .format(self.titles['getgeo']))
+        print('<input type=submit name="storetarget" value="store geo" title="{}">'
+              .format(self.titles['storegeo']))
+        print('<br><br>')
+
+        print('<table><tr>')
+        print('<td><label><b>X0<br><input type="text" name="{kx0}" value="{vx0}" title="{xyz0}" class="{flag}"></label></td>'
+              .format(kx0=c.x0.key, vx0=c.x0.val, xyz0=self.titles['xyz0'], flag='' if c.x0.chk else 'flag'))
+        print('<td><label><b>Y0<br><input type="text" name="{ky0}" value="{vy0}" title="{xyz0}" class="{flag}"></label></td>'
+              .format(ky0=c.y0.key, vy0=c.y0.val, xyz0=self.titles['xyz0'], flag='' if c.y0.chk else 'flag'))
+        print('<td><label><b>Z0<br><input type="text" name="{kz0}" value="{vz0}" title="{xyz0}" class="{flag}"></label></td>'
+              .format(kz0=c.z0.key, vz0=c.z0.val, xyz0=self.titles['xyz0'], flag='' if c.z0.chk else 'flag'))
+        print('</tr>')
+        print('<tr>')
+        print('<td><label><b>azimuth<br><input type="text" name="{kh0}" value="{vh0}" title="{azimuth}" class="{flag}"></label></td>'
+              .format(kh0=c.h0.key, vh0=c.h0.val, azimuth=self.titles['azimuth'], flag='' if c.h0.chk else 'flag'))
+        print('<td><label><b>Y1<br><input type="text" name="{ky1}" value="{vy1}" title="{xyz1}" class="{flag}"></label></td>'
+              .format(ky1=c.y1.key, vy1=c.y1.val, xyz1=self.titles['xyz1'], flag='' if c.y1.chk else 'flag'))
+        print('<td><label><b>Z1<br><input type="text" name="{kz1}" value="{vz1}" title="{xyz1}" class="{flag}"></label></td>'
+              .format(kz1=c.z1.key, vz1=c.z1.val, xyz1=self.titles['xyz1'], flag='' if c.z1.chk else 'flag'))
+        print('<tr/>')
+        print('<tr>')
+        print('<td><label><b>speed factor<br><input type="text" name="{kspeedfact}" value="{speedfact}" title="{tspeedfact}" class="{flag}"></label></td>'
+              .format(kspeedfact=c.speedfact.key, speedfact=c.speedfact.val, tspeedfact=self.titles['speedfact'], flag='' if c.speedfact.chk else 'flag'))
+        print('<td><label><b>slowdown</b> [{slowdown}]<br><input type="text" name="{kslowdown}" list="slowdowns" title="{tslowdown}" class="{flag}"></label></td>'
+              .format(kslowdown=c.slowdown.key, slowdown='' if not c.slowdown.chk else SpeedAdjust.abrv[c.slowdown.val],
+                      tslowdown=self.titles['slowdown'], flag='' if c.slowdown.chk else 'flag'))
+        print('<td><label><b>slowdown factor<br><input type="text" name="{kslowdfact}" value="{slowdfact}" title="{tslowfact}" class="{flag}"></label></td>'
+              .format(kslowdfact=c.slowdfact.key, slowdfact=c.slowdfact.val, tslowfact=self.titles['slowfact'], flag='' if c.slowdfact.chk else 'flag'))
+        print('</tr>')
+        print('<tr>')
+        print('<td><label><b>exponent<br><input type="text" name="{kex}" value="{vex}" title="{exponent}" class="{flag}"></label></td>'
+              .format(kex=c.exp.key, vex=c.exp.val, exponent=self.titles['exponent'], flag='' if c.exp.chk else 'flag'))
+        print('<td><label><b>flyby<br><input type="text" name="{kfl}" value="{vfl}" title="{flyby}"></label></td>'
+              .format(kfl=c.flyby.key, vfl=(1 if c.flyby.val else 0), flyby=self.titles['flyby']))
+        print('<td><label><b># of steps<br><input type="text" name="{kst}" value="{vst}" title="{steps}" class="{flag}"></label></td>'
+              .format(kst=c.steps.key, vst=c.steps.val, steps=self.titles['steps'], flag='' if c.steps.chk else 'flag'))
+        print('</tr>')
+        print('</table>')
+
+        print('<input type=submit name="submit" value="submit conf" title="{submitconf}">'
+              .format(submitconf=self.titles['submitconf']))
+        print('<input type=submit name="storeparms" value="store conf" title="{storeconf}">'
+              .format(storeconf=self.titles['storeconf']))
+        print('<datalist id="slowdowns">')
+        for i in SpeedAdjust.keys:
+            print('<option value="{}"/>'.format(i))
+        print('</datalist></form>')
+        self.htmltrail()
+
+    def sendconfirm(self):
+        print("""
+        <b>{}</b><br><br>
+        <div style="font-size: smaller;">
+        Flyby KML is <span class="emph">ready</span> to be opened on Google Earth (GE):<br>
+        <ul>
+        <li>for both <b>standalone-</b> and <b>web-GE</b> download KML and open file on GE</li>
+        or<br>
+        <li>on standalone-GE open KML directly via html-ref placemark http://{}/<br>
+        <span style="font-style: italic;">(download URL-placemark and place it (via open) in GE's placemarks for subsequent direct openings)</span>.</li>
+        </ul></div>
+        <form method=POST>
+        <input type="submit" name="downlkml" value="download flyby KML">
+        <input type="submit" name="downlurl" value="download URL placemark">
+        </form>
+        <div style="font-size: 66%;"><br>{conf}<br>{coor}<br></div>
+        <hr><br>
+        """.format(time.asctime(), self.c.dnldqual, conf=self.c.confcomm, coor=self.c.coorcomm))
+
+    def httpheader(self, fn):
+        print('Content-Type: application/vnd.google-earth.kml+xml')
+        print('Content-Disposition: attachment; filename="{}"'.format(fn))
+        print()
+
+    def sendfile(self, fn):
+        self.httpheader(fn)
+        sys.stdout.flush()
+        p = Popen(('cat', fn))
+        p.wait()
+        sys.stdout.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kml/kml.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,263 @@
+import sys
+import xml.sax.saxutils
+from math import *
+
+class Kml():
+    def __init__(self):
+        self.out = sys.stdout
+        self.placemark = Placemark(self)
+        self.tour = Tour(self)
+        self.playlist = Playlist(self)
+        self.flyto = FlyTo(self)
+        self.lookat = LookAt(self)
+        self.camera = Camera(self)
+        self.document = Document(self)
+        self.folder = Folder(self)
+        self.xml = Xml(self)
+
+    def documentProlog(self, fn):
+        self.xml.head()
+        self.document.head(fn)
+
+    def documentEpilog(self):
+        self.document.tail()
+        self.xml.tail()
+
+    def folderProlog(self, fn):
+        self.folder.head(fn)
+
+    def folderEpilog(self):
+        self.folder.tail()
+
+    def tourProlog(self, fn):
+        self.tour.head(fn)
+        self.playlist.head()
+
+    def tourEpilog(self):
+        self.playlist.tail()
+        self.tour.tail()
+
+    def placemarkProlog(self, fn):
+        self.xml.head()
+        self.placemark.head(fn)
+
+    def placemarkEpilog(self):
+        self.placemark.tail()
+        self.xml.tail()
+
+    def urlPlacemark(self, url, path):
+        # ref = '<a href="http://{}/index.py">{}</a>'.format(qualname, qualname)
+        ref = '<a href="{}">{}</a>'.format(url, path)
+        p = self.pr
+        p('<Placemark>')
+        p('<name>{ref}</name>'.format(ref = xml.sax.saxutils.escape(ref)))
+        p('</Placemark>')
+
+    def wait(self, dur):
+        self.pr('\n<gx:Wait><gx:duration>{}</gx:duration></gx:Wait>'.format(dur))
+
+    def comm(self, c):
+        self.pr('\n<!-- {} -->'.format(c))
+
+    def pr(self, s):
+        print(s, file=self.out)
+
+class Xml():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self):
+        self.kml.pr('<?xml version="1.0" encoding="UTF-8"?>')
+        self.kml.pr('<kml')
+        self.kml.pr('xmlns="http://www.opengis.net/kml/2.2"')
+        self.kml.pr('xmlns:gx="http://www.google.com/kml/ext/2.2"')
+        self.kml.pr('>')
+
+    def tail(self):
+        self.kml.pr('\n</kml>')
+
+class Document():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self, fn):
+        self.kml.pr('<Document>')
+        self.kml.pr('<name>{}</name>'.format(fn))
+        self.kml.pr('<open>1</open>')
+
+    def tail(self):
+        self.kml.pr('\n</Document>')
+
+
+class Folder():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self, fn):
+        self.kml.pr('<Folder>')
+        self.kml.pr('<name>{}</name>'.format(fn))
+        self.kml.pr('<open>1</open>')
+
+    def tail(self):
+        self.kml.pr('\n</Folder>')
+
+class Tour():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self, fn):
+        self.kml.pr('\n<gx:Tour>')
+        self.kml.pr('<name>{}</name>'.format(fn))
+
+    def tail(self):
+        self.kml.pr('\n</gx:Tour>')
+
+class Playlist():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self):
+        self.kml.pr('\n<gx:Playlist>')
+
+    def tail(self):
+        self.kml.wait(1)
+        self.kml.pr('\n</gx:Playlist>')
+
+class FlyTo():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self, dur, smooth):
+        self.kml.pr('<gx:FlyTo>')
+        self.kml.pr('<gx:duration>{}</gx:duration>'.format(dur))
+        self.kml.pr('<gx:flyToMode>{}</gx:flyToMode>'.format('smooth' if smooth else 'bounce'))
+
+    def tail(self):
+        self.kml.pr('\n</gx:FlyTo>')
+
+class LookAt():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def run(self, long,lat,alt,head,tilt,range):
+        self.kml.pr('\n<LookAt>')
+        self.kml.pr('<latitude>{}</latitude>'.format(lat))
+        self.kml.pr('<longitude>{}</longitude>'.format(long))
+        self.kml.pr('<altitude>{}</altitude>'.format(alt))
+        self.kml.pr('<heading>{}</heading>'.format(head))
+        self.kml.pr('<tilt>{}</tilt>'.format(tilt))
+        self.kml.pr('<range>{}</range>'.format(range))
+        self.kml.pr('<altitudeMode>relativeToGround</altitudeMode>')
+        self.kml.pr('</LookAt>')
+
+class Camera():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def run(self,long,lat,alt,head,tilt,roll):
+        self.kml.pr('<Camera>')
+        self.kml.pr('<latitude>{}</latitude>'.format(lat))
+        self.kml.pr('<longitude>{}</longitude>'.format(long))
+        self.kml.pr('<altitude>{}</altitude>'.format(alt))
+        self.kml.pr('<heading>{}</heading>'.format(head))
+        self.kml.pr('<tilt>{}</tilt>'.format(tilt))
+        self.kml.pr('<roll>{}</roll>'.format(roll))
+        self.kml.pr('<altitudeMode>relativeToSeaFloor</altitudeMode>')
+        self.kml.pr('</Camera>')
+
+    def runL(self, long, lat, alt, head, tilt, range, roll):
+        # dostává parametry LookAt, které konvertuje na parametry Camera tak, aby pohled byl stejný
+        dLO, dLA, Z = l2c(lat, head, tilt, range)
+        self.run(long + dLO, lat + dLA, Z, head, tilt, roll)
+
+class Placemark():
+    def __init__(self, kml):
+        self.kml = kml
+
+    def head(self, fn):
+        self.kml.pr('<Placemark>')
+        self.kml.pr('<name>{}</name>'.format(fn))
+        self.kml.pr('<visibility>0</visibility>')
+        self.kml.pr('<styleUrl>#PIN_YELLOW</styleUrl>')
+
+    def tail(self):
+        self.kml.pr('\n</Placemark>')
+
+class SpeedAdjust():
+    keys = ['none', 'linear', 'parabolic', 'tangential', 'exponential']
+    abrv = {'none':'none', 'linear':'lin', 'parabolic':'par', 'tangential':'tang', 'exponential':'exp'}
+
+    def __init__(self, const, koef, steps, config):
+        self.slowdowns = {'none':self.Const, 'linear':self.Linear, 'parabolic':self.Parabolic, 'tangential':self.Tangent, 'exponential':self.Exponential}
+        self.const = const
+        self.koef = koef
+        self.steps = steps
+        self.c = config
+
+    def phase(self):
+        return 1 - self.c.xi / self.c.x0.val
+
+    class Const():
+        p = 1					# empirická konstanta
+        def __init__(self, sa):
+            self.sa = sa
+        def koef(self):
+            return self.p / self.sa.const
+
+    class Linear():
+        p = 2					# empirická konstanta
+        def __init__(self, sa):
+            self.sa = sa
+        def koef(self):
+            return self.p * self.sa.koef * self.sa.phase() / self.sa.const
+
+    class Parabolic():
+        p = 2					# empirická konstanta
+        def __init__(self, sa):
+            self.sa = sa
+        def koef(self):
+            return self.p * pow(self.sa.koef * self.sa.phase(), 2) / self.sa.const
+
+    class Tangent():
+        p = 1					# empirická konstanta
+        def __init__(self, sa):
+            self.sa = sa
+        def koef(self):
+            margin = atan(self.sa.koef)		# < pi/2
+            return (1 + self.p * tan(self.sa.phase() * margin)) / self.sa.const
+
+    class Exponential():
+        p = 1					# empirická konstanta
+        def __init__(self, sa):
+            self.sa = sa
+        def koef(self):
+            return pow(e, self.p * self.sa.koef * self.sa.phase()) / self.sa.const
+
+def canonGeodetic(a):
+    if(a > 360 or a < -360): a = a % 360
+    if(a > 180): a = a - 360
+    if(a < -180): a = a + 360
+    return a
+
+def can360(a360):
+    if(a360 < 0): a360 = a360 + 360
+    elif(a360 > 360): a360 = a360 - 360
+    return a360
+
+def can180(a180):
+    if(a180 < -180): a180 = a180 + 360
+    elif(a180 > 180): a180 = a180 - 360
+    return a180
+
+def l2c(lat, head, tilt, range):
+    RZ = 6378000
+    RL = RZ * cos(radians(lat))
+    Hc = can360(head + 180)
+    Z = range * cos(radians(tilt))
+    d = sqrt(pow(range,2) - pow(Z,2))
+    dLA = degrees(atan(d * cos(radians(Hc)) / RZ))
+    dLO = degrees(atan(d * sin(radians(Hc)) / RL))
+    return dLO, dLA, Z
+
+def deb(s):
+    print('+++ {}'.format(s), file=sys.stderr)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kml/kmlcgi.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,30 @@
+#!/usr/bin/python3
+# coding=utf8
+
+import sys
+import os
+import time
+import config
+import hh_http
+import flyby
+from kml import deb
+
+# for s in ('PYTHONIOENCODING', 'LC_ALL'):
+# 	if s in os.environ:
+# 		deb(os.environ[s])
+# for s in os.environ:
+#     deb('ENV[{}]={}'.format(s, os.environ[s]))
+
+if(not 'HTTP_HOST' in os.environ):
+    sys.exit(1)
+c = config.Config()
+h = hh_http.Http(c)
+fl = flyby.Flyby(c)
+if c.configReq:             	# on config request (...kml...) configure tour
+    if h.parsehttp(''):			# syntax OK
+        fl.tour()				# generate Google Earth tour
+        h.sendform(confirm=True)
+    else:
+        h.sendform(confirm=False)
+elif c.dnldReq:	                # on download request (...flyby...) send KML-file
+    h.sendfile(c.kmlfile)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kml/targets.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,20 @@
+class Hradčany():
+    name = 'Hradčany'
+    lat = 50.0908833
+    long = 14.4005364
+
+class Onekotan():
+    name = 'Onekotan'
+    lat =  49.351015
+    long =  154.708890
+
+class Memento():
+    name = 'memento mori'
+    lat = 50.0947447
+    long = 14.4159806
+
+class Target():
+    def __init__(self,name,lat,long):
+        self.name = name
+        self.lat = float(lat)
+        self.long = float(long)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/loxodroma.py	Mon May 18 08:48:51 2020 +0200
@@ -0,0 +1,66 @@
+#!/usr/bin/python3
+# coding=utf8
+
+import sys, math
+from kml import *
+
+fn = "loxodroma"
+
+long = 0				# výchozí geodetická délka - stupně
+longStep = 5
+maxSteps = 9999
+lat = 87				# výchozí geodetická šířka - stupně
+minPoleDiff = 1			# minimální vzdálenost od pólu - stupně
+R = 6378 * 1000			# zemský poloměr v metrech
+azim = 105
+tilt = 60
+range = 4800 * 1000		# metry
+speed = 1000 * 1000		# m/s		rychlost přesunu pohledu
+durMin = 0.1			# secs		minimální doba kroku
+durMax = 1				# secs		maximální doba kroku
+
+def loxClong():
+	azimR = math.radians(azim)
+	latR = math.radians(lat)
+	longStepR = math.radians(longStep)
+	latNR = math.asin(math.tanh(longStepR / math.tan(azimR) + math.atanh(math.sin(latR))))
+	latN = math.degrees(latNR)
+	longN = canonGeodetic(long + longStep)
+	move = R * math.fabs(latNR - latR) / math.cos(azimR)	# délka posunu na povrchu koule v metrech
+	dur = math.fabs(move) / speed		# doba přeletu
+	if(dur < durMin): dur = durMin
+	elif(dur > durMax): dur = durMax
+	return (longN, latN, dur, move)
+
+def list():
+	global long, lat
+	k = 0
+	while((90 - math.fabs(lat)) >= minPoleDiff and k < maxSteps):
+		(longN, latN, dur, move) = loxClong()
+		print('{}\t{}\t{}\t{}'.format(k,longN,latN, move))
+		long = longN
+		lat = latN
+		k = k + 1
+
+def run():
+	global long, lat
+	k = 0
+
+	FlyTo.head(5, False)
+	LookAt.run(long, lat, 0, azim, tilt, range)
+	FlyTo.tail()
+
+	while((90 - math.fabs(lat)) >= minPoleDiff and k < maxSteps):
+		(longN, latN, dur, move) = loxClong()
+		FlyTo.head(dur, True)
+		comm('k={}, long={}, lat={}, longN={}, latN={}, move={}'.format(str(k), str(long), str(lat), str(longN), str(latN), str(move)))
+		k = k + 1
+		LookAt.run(longN, latN, 0, azim, tilt, range)
+		FlyTo.tail()
+		long = longN
+		lat = latN
+
+# list()
+tourProlog(fn)
+run()
+tourEpilog()