#!/usr/bin/env ruby21 # -*- coding: utf-8 -*- # gpx-photo.rb - (C)2018 by HIROSE, Yuuji # http://www.gentei.org/~yuuji/software/gpx-photo/ # Last modified Sun Sep 24 21:23:35 2023 on firestorm # $HGid: gpx-photo.rb,v 1165:e1bf36b3456c 2023-10-09 17:35 +0900 yuuji $ # # NOTE: This script uses the following software. # Ruby, jhead, GPS Babel require 'time' require 'open3' #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 = nil quiet = false gmap = false slon = slat = cuthead = cuttail = nil cuth_r = cutt_r = 800.0 taiofs = Time.gm(2014).to_i - 1388534400 # 1388534400==2014/1/1 @ nonTAI system commentfile = nil comment = Hash.new logs = Array.new jpg = Array.new lonlatround = 4 # Round digits after decimal point while ARGV[0] case ARGV[0] when /-g/ gmap = true when /^-o/ outfile = ARGV[1]; ARGV.shift when /^-C/ commentfile = ARGV[1]; ARGV.shift when /^-c/ colors = ARGV[1].split(","); ARGV.shift when /^-T/ taiofs = ARGV[1].to_i; ARGV.shift when /^-t/ title = ARGV[1]; ARGV.shift when /^-z/ zoom = ARGV[1]; ARGV.shift when /^-r/ lonlatround = ARGV[1].to_i; ARGV.shift when /^-w/ weight = ARGV[1]; ARGV.shift when /^-p(\d+)?/ cuthead = true if $1 cuth_r = $1.to_f elsif ARGV[1] && /^(\d+)$/ =~ ARGV[1] cuth_r = ARGV[1].to_f; ARGV.shift end when /^-P(\d+)?/ cuttail = true if $1 cutt_r = $1.to_f elsif ARGV[1] && /^(\d+)$/ =~ ARGV[1] cutt_r = ARGV[1].to_f; ARGV.shift end when "-q" quiet = true when /\.gpx$/i logs << ARGV[0] when /\.jpe?g/i jpg << ARGV[0] else STDERR.printf("Argument [%s], ignored\n", ARGV[0]) end ARGV.shift end if logs.empty? && jpg.empty? myname = File.basename($0) STDERR.print(<<_EOF_) #{myname} - Making photo-map from GPX logs and photos Usage: % #{myname} [options] [GPXfiles] [Photo files with EXIF] Options are as follows: -g GoogleMaps for additional layer -o File Output HTML files to `File' -C CmntTxt Read `CmntTxt' as comment txt(*1) -c Col1,Col2,.. Set GPX path color list to comma separeted list -t Title Set HTML title to `Title' -z Zoom Set default zoom level without GPX 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 `FileNameComment'. Comment part should be written within a line. (eg.) file1.jpg Comment 1 file2.jpg Comment 2 file3.jpg Comment 3 _EOF_ #' exit 0 end trk = Array.new outfile or outfile = if logs[0] logs[0] else File.basename($0) end.sub(/\.[a-z]+$/i, ".html") outdir = File.dirname(outfile) # for calculating distance def distance(lon0, lat0, lon1, lat1) 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 ENV["HUBENY"] then # 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 Math::sqrt((dlat*m)**2 + (dlon*n*Math::cos(avlat))**2) else rSphere*Math::sqrt((dlon*Math::cos(avlat))**2 + dlat**2) end end marks = "" legend = "" logs.each do |gpx| llon = llat = nil dist = 0.0 thistrk = [] gpxstat = File.stat(gpx) ntgpx = gpxstat.dev.to_s(36) + "-" + gpxstat.ino.to_s(36) + ".ogpx" Open3.popen3("gpsbabel -i gpx -o ricoh -f #{gpx} -x simplify,error=4m -F -") do |i, g, e| # 139.862170,38.942190,-2.000000,-2.000000,01-07-2014 00:51:31 i.close Thread.new {true while e.gets} while track=g.gets lon, lat, e1, e2, tm = track.split(",") tmf = Time.parse(tm.chomp+" +0000").to_f thistrk << [lon.to_f, lat.to_f, e1.to_f, e2.to_f, tmf] llon and dist += distance(llon.to_f, llat.to_f, lon.to_f, lat.to_f) llon, llat = lon, lat end end startstop = "" 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 break if (i+=1)>=thistrk.length-1 end # p i, thistrk[i], distance(lon0, lat0, thistrk[i][0], thistrk[i][1]) if i != thistrk.length-1 # if found start = Time.at(thistrk[i][-1]+taiofs).utc.strftime("%Y%m%d%H%M%S") startstop = sprintf(" -x track,start=%s", start) end end if cuttail lonF, latF, i = thistrk[-1][0], thistrk[-1][1], thistrk.length-1 while distance(lonF, latF, thistrk[i][0], thistrk[i][1]) < cutt_r/1000 break if (i-=1) < 0 end ## p i, thistrk[i], distance(lon0, lat0, thistrk[1][0], thistrk[1][1]) if i >= 0 # if found stop = Time.at(thistrk[i][-1]+taiofs).utc.strftime("%Y%m%d%H%M%S") startstop += (/-x/ =~ startstop ? "" : " -x track") + sprintf(",stop=%s", stop) end end open(File.join(outdir, ntgpx), "w") do |wgpx| readgpx = sprintf("| gpsbabel -i gpx -o gpx -f %s%s -x simplify,error=4m -F -", gpx, startstop) # p readgpx open(readgpx, "rb") do |g| # Remove sensitive trk information wgpx.write(g.read.gsub(%r!<(time|speed)>[^<]+!, "")) end end trk += thistrk diststr = (dist > 1.0 ? sprintf("(%.1fkm)", dist) : sprintf("(%dm)", dist*1000)) title = gpx.sub(/\.[a-z]+$/i, "") + diststr colors << (color = colors.shift) marks += sprintf(<<_EOF_, title, ntgpx, color, thistrk[0][0], thistrk[0][1]) addGPX("%s", "%s", "%s", %f, %f);\n _EOF_ legend += sprintf(<<_EOF_, color, title) %s _EOF_ end # Read comment file if commentfile IO.readlines(commentfile).each do |l| jpgfile, cmt = l.scan(/(.*\.jpe?g)\s+(.*)/i)[0] if jpgfile && cmt && test(?s, jpgfile) comment[jpgfile] = cmt.chomp end end end longedge = 400 photo = Hash.new i = 0 trkmin = trk.sort_by{|x| x[-1]}[0][-1] trkmax = trk.sort_by{|x| -x[-1]}[0][-1] #printf("time range %s to %s", Time.at(trkmin).to_s, Time.at(trkmax)) jpegpos = Hash.new jpg.each do |ph| exif = `jhead #{ph}`.encode("binary", "binary").split("\n") unless (datetime = (exif.grep(%r,^Date/Time,)[0] || exif.grep(%r,^File date,)[0])) STDERR.printf("[%s] has no Exif Date/Time information, skipped\n", ph) next end pt = datetime.sub(/^[^:]+:\s*/, '').sub(":", "-").sub(":", "-") ptime = Time.parse(pt).to_f res = exif.grep(%r,^Resolution,)[0].sub(/^[^:]+:\s*/, '') #model = exif.grep(%r,^Camera model,i)[0].sub(/^[^:]+:\s*/, '') #unless model # STDERR.printf("[%s] is not an PHOTO file, skipped", ph) # next #end # Calculate width/height for displaying (not shrikned actually) width, height = res.scan(/(\d+) x (\d+)/)[0] x, y = width.to_i, height.to_i if x > y w = longedge h = w*y/x else h = longedge w = h*x/y end next if pt == "" gpslat = exif.grep(%r,GPS Latitude : ,)[0] gpslon = exif.grep(%r,GPS Longitude: ,)[0] gpsalt = exif.grep(%r,GPS Altitude : ,)[0] if gpslat && gpslon gpslat = (/[N?]\s+(\d+)d\s+([0-9.]+)m\s+([.0-9]+)s/ =~ gpslat ? $1.to_f + $2.to_f/60 + $3.to_f/3600 : nil) gpslon = (/[E?]\s+(\d+)d\s+([0-9.]+)m\s+([.0-9]+)s/ =~ gpslon ? $1.to_f + $2.to_f/60 + $3.to_f/3600 : nil) gpsalt = (/([.0-9]+)m/ =~ gpsalt ? $1.to_f : nil) end html = ph.sub(/\.jpe?g/, ".html") link = (test(?s, html) ? html : ph) cmt = sprintf(" comments['%s'] = ['%s', '%s', %d, %d];\n", ph, (pt + "\n" + (comment[ph]||"")).gsub("'", "'").sub("\n", "
"), link, w, h) marks += cmt if gpslat && gpslon # When photo itself has geoinfo trk << [gpslon, gpslat, gpsalt, gpsalt, Time.parse(pt).to_f] photo[ph] = trk[-1] else if ptime < trkmin || ptime > trkmax STDERR.printf("[%s] is NOT taken within GPX trip term, skipped\n", ph) marks.chomp! marks += "// Skipped\n" next end photo[ph] = trk.sort_by {|x| (x[-1] - ptime)**2}[0] # get nearest point end rlon = photo[ph][0].round(lonlatround) rlat = photo[ph][1].round(lonlatround) rpos = rlon.to_s + "," + rlat.to_s jpegpos.has_key?(rpos) || jpegpos[rpos] = Array.new jpegpos[rpos] << [ph, w, h] i+=1 end.join jpegpos.each do |pos, jpegs| rlon, rlat = pos.split(",") files = jpegs.collect {|j| j[0]} marks += sprintf(<<_EOF_, rlat, rlon, files.join('", "')) addPopUp(%f, %f, ["%s"]); _EOF_ end quiet or printf("Output to \"%s\"\n", outfile) trk.sort_by!{|i| i[-1]} lons = trk.collect{|t| t[0]}.sort lats = trk.collect{|t| t[1]}.sort lon = (lons[0]+lons[-1])/2.0 lat = (lats[0]+lats[-1])/2.0 trk_sblon = trk.sort_by{|i| i[0]} trk_sblat = trk.sort_by{|i| i[1]} if trk.length > 1 minlon, minlat = trk_sblon[0][0], trk_sblat[0][1] maxlon, maxlat = trk_sblon[-1][0], trk_sblat[-1][1] marks += sprintf(<<_EOF_, minlat, minlon, maxlat, maxlon) map.fitBounds([[%f, %f], [%f, %f]]); _EOF_ else marks += sprintf(" map.setView(latlon, %d);", zoom) end open(outfile, "w") do |w| DATA.each_line do |l| w.print l.gsub("_LON_", lon.to_s). gsub("_LAT_", lat.to_s). gsub("_TITLE_", title). # gsub("_LOG_", logs[0]). gsub("_FMT_", "GPX"). # gsub("_ZOOM_", zoom). gsub("_MARKS_", marks). gsub("_WEIGHT_", weight). gsub("_LEGEND_", legend) end if gmap w.print(<<_EOF_) _EOF_ end end ########################################################################### __END__ _TITLE_

_LEGEND_