#!/usr/bin/env ruby21
# -*- coding: utf-8 -*-
# gpx-photo.rb - (C)2018 by HIROSE, Yuuji <yuuji(at)yatex.org>
# 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 `FileName<BLANK>Comment'.
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)>[^<]+</\1>!, ""))
    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)
<span style="border: thick solid %s">%s</span>
_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("'", "&#39;").sub("\n", "<br>"),
                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_)
<script src="http://maps.google.com/maps/api/js?v=3.2&sensor=false"></script>
<script src="https://rawgithub.com/shramov/leaflet-plugins/master/layer/tile/Google.js"></script>
_EOF_
  end
end

###########################################################################
__END__
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://www.yatex.org/libcache/leaflet-0.7.6/leaflet.css" />
<style type="text/css">
<!--
 div#map {height: 700px;}
 -->
</style>
<script src="https://www.yatex.org/libcache/leaflet-0.7.6/leaflet.js"></script>
<!-- <script src="https://raw.githubusercontent.com/mpetazzoni/leaflet-gpx/master/gpx.js"></script> -->
<script src="https://www.yatex.org/libcache/gpx.js"></script>
    <title>_TITLE_</title>
<script type="text/javascript" charset="UTF-8">
<!--
function init() {
    var useOSM = true;
    var latlon = [_LAT_, _LON_];
    var map = L.map('map')
    var tileLayer;
    var gmapLayer;
    if (L.Google) {
      gmapLayer = new L.Google("ROADMAP");
      map.addLayer(gmapLayer);
    }
    tileLayer = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
        attribution : '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors | Generated with <a href="http://www.gentei.org/~yuuji/software/gpx-photo/">gpx-photo</a>'
    });
    tileLayer.addTo(map);
    var gsiLayer =
    L.tileLayer('//cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
        attribution:
	'<a href="http://maps.gsi.go.jp/development/ichiran.html">国土地理院</a>'
	});
    var gsiPhotoLayer =
    L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg', {
        attribution:
	'<a href="http://maps.gsi.go.jp/development/ichiran.html">全国最新写真(国土地理院)</a>'
	});
    L.control.scale().addTo(map);

    // Add marker
    function addPopUp(lat, lon, imgs) {
        var latlon = [lat, lon];
        var marker = L.marker(latlon).addTo(map);
        var msg, cmt, link, w, h;
        if (!imgs.length) {
          imgs = [imgs];
        }
        msg = ""; cmt = "";
        for (var i=0, cmt=""; i < imgs.length; i++) {
          img = imgs[i];
          msg = comments[img][0];
          link = comments[img][1];
          w = comments[img][2];
          h = comments[img][3];
          cmt += "<p>" + (msg ? msg : img) + "<br>" +
                '<a target="_blank" href="'+link+'"><img src="'+
                img+'" width="'+w+'" height="'+h+'"></a></p>'
        }
        marker.bindPopup(
            cmt,
            {maxWidth: 500, maxHeight: 400});
    }


    var comments = new Array();
    var gpxlayers = {};
    function addGPX(title, gpx, color, lon0, lat0) {
	var baseurl = 'https://rawgithub.com/mpetazzoni/leaflet-gpx/master/';
	var gpxLayer = new L.GPX(gpx, {
      	    async: true,
	    marker_options: {
		startIconUrl: null, // baseurl+'pin-icon-start.png',
		endIconUrl: null,   // baseurl+'pin-icon-end.png',
		shadowUrl: baseurl+'pin-shadow.png'
	    },
	    polyline_options: {
		color: color,
		weight: _WEIGHT_,
	    }
//	}).on('loaded', function(e) {
//	    var gpx = e.target;
//	    map.fitBounds(gpx.getBounds());
	}).addTo(map);
	gpxlayers[title] = gpxLayer;
	
    }

_MARKS_

    var baseMaps;
    if (gmapLayer) {
	baseMaps = {"GoogleMaps": gmapLayer, "OSM": tileLayer};
    } else {
	baseMaps = {
          "OpenStreetMap": tileLayer,
          "国土地理院": gsiLayer,
          "全国最新写真(国土地理院)": gsiPhotoLayer,
	};
    }
    L.control.layers(baseMaps, gpxlayers).addTo(map);
}
// -->
</script>
</head>

<body onload="init();">
    <p>_LEGEND_</p>
    <div id="map"></div>
</body>
</html>