#!/usr/bin/env python3 # -*- coding: utf-8 -*- # geomapper.py - (C)2016-2019 by HIROSE, Yuuji <yuuji(at)yatex.org> # http://www.gentei.org/~yuuji/software/geomapper/ # $HGid: geomapper.py,v 7:eaf4f4b28afd 2019-08-13 10:25 +0900 yuuji $ # # NOTE: This script required the following software. # Python, jhead, GPS Babel import sys, re, time, datetime, os.path, json, pprint, shutil from subprocess import Popen, PIPE, check_output omnivore0 = """ <script src="https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-omnivore/v0.3.1/leaflet-omnivore.min.js"></script> <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise"></script> """.strip() omnivore = """ <script src="https://www.yatex.org/libcache/leaflet-omnivore/tags/v0.3.2/leaflet-omnivore.min.js"></script> """.strip() bing = """ <script src="https://www.yatex.org/libcache/leaflet-plugins-2.0.0/layer/tile/Bing.js"></script> """.strip() google0 = """ <script src="https://maps.google.com/maps/api/js?key=AIzaSyCKJ-CvuAuD9QovJ0EwLfsxGkERnF-T5i0&v=3.20&sensor=false"></script> <script src="https://rawgithub.com/shramov/leaflet-plugins/master/layer/tile/Google.js"></script> """.strip() google = """ <script src="https://maps.google.com/maps/api/js?key=AIzaSyCKJ-CvuAuD9QovJ0EwLfsxGkERnF-T5i0&v=3.20&sensor=false"></script> <script src="https://www.yatex.org/libcache/leaflet-plugins-2.0.0/layer/tile/Google.js"></script> """.strip() gpx = """ <script src="https://www.yatex.org/libcache/gpx.js"></script> """.strip() #tmpl = File.expand_path(File.basename($0, ".rb")+".tmpl", File.dirname($0)) title = "Photo Map" colors = ["red", "green", "blue", "purple", "yellow", "cyan", "magenta"] zoom = "10" weight = "8" outfile = None quiet = False usegmap = False usebing = True usexml = False slon = slat = cuthead = cuttail = None cuth_r = cutt_r = 800.0 # taiofs: 1388534400==2014/1/1 @ nonTAI system taiofs = int(time.mktime((2014,1,1,0,0,0,0,0,0))-time.timezone)-1388534400 commentfile = None comment = {} logs = [] ologs = [] jsons = [] jpg = [] lonlatround = 4 # Round digits after decimal point featurefile = "output.geojson" umapjs="umappop.js" # Argument parsing ARGV=sys.argv argv0 = ARGV.pop(0) while ARGV: a=ARGV[0] if a=='-g': usegmap = True elif a=='-b': usebing = True elif a=='-o': outfile = ARGV[1]; ARGV.pop(0) elif a=='-C': commentfile = ARGV[1]; ARGV.pop(0) elif a=='-c': colors = ARGV[1].split(","); ARGV.pop(0) elif a=='-T': taiofs = int(ARGV[1]); ARGV.pop(0) elif a=='-t': title = ARGV[1]; ARGV.pop(0) elif a=='-z': zoom = ARGV[1]; ARGV.pop(0) elif a=='-r': lonlatround = int(ARGV[1]); ARGV.pop(0) elif a=='-w': weight = ARGV[1]; ARGV.pop(0) elif a=='-j': umapjs = ARGV[1]; ARGV.pop(0) elif a[0:2]=='-p' or a[0:2]=='-P': m=re.match("-([Pp])(\d+)?", a) if m.group(1) == 'p': cuthead = True else: cuttail = True if m.group(2): if m.group(1)=='p': cuth_r = float(m.group(1)) else: cutt_r = float(m.group(1)) elif len(ARGV)>1 and re.match("^(\d+)$", ARGV[1]): if m.group(1)=='p': cuth_r = float(ARGV[1]); ARGV.pop(0) else: cutt_r = float(ARGV[1]); ARGV.pop(0) elif a=="-q": quiet = True elif re.search("\.(gpx|kml)$", a, re.I): logs.append(ARGV[0]); usexml = True from xml.dom import minidom elif re.search("\.jpe?g$", a, re.I): jpg.append(ARGV[0]) elif re.search("\.(geojson|umap)", a, re.I): color = colors.pop(0) jsons.append({'file': ARGV[0], 'color': color, 'weight': weight}) colors.append(color) else: sys.stderr.write("Argument [%s], ignored\n" % ARGV[0]) ARGV.pop(0) myname = os.path.basename(argv0) mydir = os.path.dirname(argv0) if not logs and not jpg and not jsons: sys.stderr.write(""" {myname} - Making photo-map from GPX/KML logs and photos Usage: % {myname} [options] [GPSLogfiles] [Photo files with EXIF] Options are as follows: -g GoogleMaps for additional layer -b BingMaps for additional layer -j UMAPPOPjs Use UMAPPOPjs javascript file instead of {umapjs} -o File Output HTML files to `File' -C CmntTxt Read `CmntTxt' as comment txt(*1) -c Col1,Col2,.. Set GPSLog path color list to comma separeted list -t Title Set HTML title to `Title' -z Zoom Set default zoom level without GPS log -r D Round Lon/Lat off to (D-1) decimal places -p Privacy mode: cut GPS log within {cuth_r}m at start -pN Privacy mode: cut GPS log within `N` meter at start -P Privacy mode: cut GPS log within {cutt_r}m at goal -PN Privacy mode: cut GPS log within `N' meter at goal -T TAIoffset Set TAI offset(defaults to {taiofs}) Note that JPEG file SHOULD contain EXIF information. JPEG files are directly linked to PopUp-s in a map. You might want to shrink jpeg files suitable for Web. Beware your graphic editor might drop EXIF after resizing operations. To keep EXIF in shrinked jpeg files, consider to use these: GIMP (w/EXIF extension) http://www.gimp.org/ rjpg http://www.gentei.org/~yuuji/software/rjpg (*1) Format of comment text is a repetition of `FileName<BLANK>Comment'. Comment part should be written within a line. (eg.) file1.jpg Comment 1 file2.jpg Comment 2 file3.jpg Comment 3 """.format(**locals())) quit() outfile = os.path.splitext( outfile or (logs and logs[0]) or (jsons and jsons[0]['file']) or myname )[0] + ".html" outdir = os.path.dirname(outfile) # for calculating distance def distance(lon0, lat0, lon1, lat1): import math, os rx = 6378.137 # (km) ry = 6356.752314 # (km) esq=(rx**2-ry**2)/rx**2 rSphere = 40000.0/2/math.pi dlon = (lon1-lon0)*math.pi/180 dlat = (lat1-lat0)*math.pi/180 avlat = (lat0+lat1)/2.0*math.pi/180 if os.environ.get("HUBENY"): # http://hp.vector.co.jp/authors/VA002244/yacht/geo.htm w = math.sqrt(1-esq*math.sin(avlat)**2) m = rx*(1-esq)/w**3 n = rx/w return math.sqrt((dlat*m)**2 + (dlon*n*math.cos(avlat))**2) else: return rSphere*math.sqrt((dlon*math.cos(avlat))**2 + dlat**2) def n2s(n, base=10): bs = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" if base>len(bs) or base<2: return None s="" while n>0: r = n%base s = bs[r] + s n //= base return s trk = [] # Keep trk list legend = "" for gpslog in logs: fmt = os.path.splitext(gpslog)[1][1:] llon = llat = None dist = 0.0 thistrk = [] gpsstat = os.stat(gpslog) ntgps = n2s(gpsstat.st_dev, 36)+"-"+n2s(gpsstat.st_ino, 36)+".o"+fmt cmd = ['gpsbabel', '-i', fmt, '-o', 'ricoh', '-f', gpslog, '-x', 'simplify,error=4m', '-F', '-'] with Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) as p: # 139.862170,38.942190,-2.000000,-2.000000,01-07-2014 00:51:31 # import fcntl # fl = fcntl.fcntl(p.stdout.fileno(), fcntl.F_GETFL) # fl = fl | os.O_NONBLOCK # fcntl.fcntl(p.stdout.fileno(), fcntl.F_SETFL, fl) # fl = fcntl.fcntl(p.stderr.fileno(), fcntl.F_GETFL) # fl = fl | os.O_NONBLOCK # fcntl.fcntl(p.stderr.fileno(), fcntl.F_SETFL, fl) # setblocking(p.stderr) def readstderr(fd): while True: try: line = fd.readline() # sys.stderr.write("E") except Exception as e: # str(type(e))), str(e.args)), str(e)) break; import threading threading.Thread(target=readstderr, args=(p.stderr,)).start() while True: track=p.stdout.readline() if track: lon, lat, e1, e2, tm = track.decode('ascii').split(",") tmf = datetime.datetime.strptime(tm.strip(), "%d-%m-%Y %H:%M:%S") tmg = time.mktime(tmf.timetuple())-time.timezone thistrk.append([float(lon), float(lat), float(e1), float(e2), float(tmg)]) if llon: dist += distance(float(llon), float(llat), float(lon), float(lat)) llon, llat = lon, lat #elif p.stderr.readline(): # sys.stderr.write("E") # put E without NL else: break # EOF starttm = 0 stoptm = int(datetime.datetime.now().strftime("%s")) # Set max time if cuthead: # p Time.at(thistrk[0][-1]).utc.strftime("%Y%m%d%H%M%H") lon0, lat0, i = thistrk[0][0], thistrk[0][1], 1 while distance(lon0, lat0, thistrk[i][0], thistrk[i][1]) < cuth_r/1000: i+=1 if i>=len(thistrk)-1: break if i != len(thistrk)-1: # if found newtrktime = thistrk[i][-1]+taiofs starttm = newtrktime if cuttail: lonF, latF, i = thistrk[-1][0], thistrk[-1][1], len(thistrk)-1 while distance(lonF, latF, thistrk[i][0], thistrk[i][1]) < cutt_r/1000: i-=1 if i < 0: break if i >= 0: # if found newtrktime = thistrk[i][-1]+taiofs stoptm = newtrktime xml = minidom.parse(gpslog) node = xml.getElementsByTagName("Document") or \ xml.getElementsByTagName("trk") or \ xml.getElementsByTagName("gpx") if node: name = node[0].getElementsByTagName("name") label = (name[0].childNodes[0].data if name else os.path.splitext(gpslog)[0]) if name: name[0].childNodes[0].data = label else: newname = xml.createElement("name") newname.appendChild(xml.createTextNode(label)) node[0].insertBefore(newname, node[0].firstChild) # Remove length==1 trkseg, bacause omnivore hangs for node in xml.getElementsByTagName("trkseg"): # print("LEN=", len(node.childNodes)) if 1 == len(node.getElementsByTagName("trkpt")): node.parentNode.removeChild(node) # Remove privacy information rmcnt = 0 for node in xml.getElementsByTagName("trkpt"): for t in node.getElementsByTagName("time"): utc = t.firstChild.data.replace("Z", "") # Ugly... dtm = datetime.datetime.strptime(utc, "%Y-%m-%dT%H:%M:%S.%f") epotm = time.mktime(dtm.timetuple())-time.timezone if epotm < starttm or epotm > stoptm: rmcnt += 1 # print("%3d:Removing utc=%s, tm=%s\n" % (rmcnt, utc, t.firstChild.data)) node.parentNode.removeChild(node) if not quiet and rmcnt > 0: sys.stderr.write("%d points removed for privacy\n" % rmcnt) # Remove naive information times = (xml.getElementsByTagName("time") + xml.getElementsByTagName("speed")) for node in times: node.parentNode.removeChild(node) with open(ntgps, "w") as nt: # nt.write(xml.toprettyxml(' ', '\n', 'utf-8').decode()) nt.write(xml.toxml('utf-8').decode()) trk += thistrk diststr = "(%.1fkm)" % dist if dist > 1.0 else "(%dm)" % (dist*1000) title = re.sub(re.compile('\.[a-z]+$', re.I), "", gpslog) + diststr color = colors.pop(0) colors.append(color) legend += ('<span style="border: solid thick %s">%s</span>'%(color, title)) ologs.append({'file': ntgps, 'color': color, 'weight': weight}) # colors << (color = colors.shift) ###### Comment file parsing if commentfile: with open(commentfile) as cm: for line in cm: m = re.findall('(\S+)\s+(.*)', line) if m: f, c = m[0] if os.path.exists(f): comment[f] = c longedge = 400 photo = {} i = 0 #printf("time range %s to %s", Time.at(trk[0][-1]).to_s, Time.at(trk[-1][-1])) jpegpos = {} jpeginf = {} for ph in jpg: exif = check_output(['jhead', ph]).decode("utf-8").split("\n") pt = ([x for x in exif if re.match('Date/Time', x)] or [x for x in exif if re.match('File date', x)]) if not pt: sys.stderr.write("[%s] has no Exif Date/Time information, skipped\n" % ph) next pt = re.sub('^[^:]+:\s*', '', pt[0]) if pt == '': next pt = re.sub('(\d\d\d\d):(\d\d):(\d\d)', '\\1-\\2-\\3', pt) # sigh...for poooor time handling ptime = datetime.datetime.strptime(pt, "%Y-%m-%d %H:%M:%S") ptime = ptime.timetuple() # We assume the photo timestamp is ptime = float(time.mktime(ptime)) # represented as localtime res = [x for x in exif if x.startswith('Resolution')]\ [0].split(':')[1].strip() # Calculate width/height for displaying (not shrikned actually) width, height = res.split('x') x, y = int(width), int(height) if x > y: w = longedge h = w*y/x else: h = longedge w = h*x/y gpslat = [x for x in exif if x.startswith("GPS Latitude")] gpslon = [x for x in exif if x.startswith("GPS Longitude:")] gpsalt = [x for x in exif if x.startswith("GPS Altitude")] # GPS Latitude : N 38d 54m 54.187s # GPS Longitude: E 139d 50m 14.5284s if gpslat and gpslon: m = re.search('[N?]\s+(\d+)d\s+([0-9.]+)m\s+([.0-9]+)s', gpslat[0]) gpslat = (float(m.group(1)) + float(m.group(2))/60 + float(m.group(3))/3600) if m else None m = re.search('[E?]\s+(\d+)d\s+([0-9.]+)m\s+([.0-9]+)s', gpslon[0]) gpslon = (float(m.group(1)) + float(m.group(2))/60 + float(m.group(3))/3600) if m else None if gpsalt: m = re.search('\s+([0-9.]+)m', gpsalt[0]) gpsalt = float(m.group(1)) if m else None else: gpsalt = None html = os.path.splitext(ph)[0] + ".html" link = html if os.path.exists(html) else ph jpeginf[ph] = {"desc": comment[ph] if comment.get(ph) else pt} if gpslat and gpslon: # When photo itself has geoinfo trk.append([gpslon, gpslat, gpsalt, gpsalt, ptime]) photo[ph] = trk[-1] else: if ptime < trk[0][-1] or ptime > trk[-1][-1]: sys.stderr.write( "[%s] is NOT taken within GPS trip term, skipped\n" % ph) next photo[ph] = sorted(trk, key=lambda x: (x[-1]-ptime)**2)[0] # print(photo[ph]) rlon = round(photo[ph][0], lonlatround) rlat = round(photo[ph][1], lonlatround) rpos = str(rlon) + "," + str(rlat) if not jpegpos.get(rpos): jpegpos[rpos] = [] jpegpos[rpos].append([ph, w, h]) i+=1 features = [] ## print(jpeginf) for pos, jpegs in jpegpos.items(): rlon, rlat = pos.split(",") files = [j[0] for j in jpegs] desc = "\n".join([(jpeginf[j]["desc"] + "\n" + "[[%s|{{%s}}]]"%(j,j)) for j in files]) # print(jpegs) name = ", ".join([os.path.splitext(j[0])[0] for j in jpegs]) features.append({ "type": "Feature", "geometry": { "type": "Point", "coordinates": [float(rlon), float(rlat)]}, "properties": {"name": name, "description": desc} }) geojson = { "type": "FeatureCollection", "name": "Photo List", "features": features } if features: with open(featurefile, "w") as o: # Unsetting ensure_ascii for KanjiChars o.write(json.dumps(geojson, indent=1, ensure_ascii=False)) jsons.append({'file':featurefile}) template = """ <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <!-- <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" /> --> <link rel="stylesheet" href="https://www.yatex.org/libcache/leaflet-1.3.2/leaflet.css" /> <!-- <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script> --> <script src="https://www.yatex.org/libcache/leaflet-1.3.2/leaflet.js"></script> _BING_ _GOOGLE_ _GPX_ <style type="text/css"> <!-- img.popup {width: 240px;} div#map {height: 700px;} .leaflet-popup-content {max-height: 300px; overflow: auto;} .leaflet-popup-content p {white-space: pre-wrap;} --> </style> <title>_TITLE_</title> <script type="text/javascript" src="%s" charset="utf-8"></script> </head> <body onload="umappop(_INIT_);"> <p>_LEGEND_ <span id="latlng"></span></p> <div id="map"></div> </body> </html> """.strip() % umapjs # src="umappop.js" # print(diststr) initargs = pprint.pformat(jsons+ologs) if not quiet: print('Output to "%s" - %s' % (outfile, initargs)) with open(outfile, "w") as w: _g = google if usegmap else "" _b = bing if usebing else "" _x = omnivore if usexml else "" w.write(template.replace("_TITLE_", title). replace("_LEGEND_", legend). replace("_INIT_", initargs). replace("_GOOGLE_", _g). replace("_BING_", _b). replace("_GPX_", _x)) umappop = os.path.join(outdir, umapjs) if not os.path.exists(umappop): try: relp = os.path.relpath(os.path.join(mydir, umapjs), outdir) os.link(relp, umappop) except Exception as e: try: shutil.copy(relp, umappop) except Exception as e: sys.stderr.write("Cannot create symlink or file of "+umapjs)