<commit_before>
<html>
<head>
<meta charset="utf-8">
<title>GPX to TCX Converter</title>
<meta property="og:title" content="GPX to TCX Converter" />
<meta property="og:description" content="GPXからTCXファイルに変換するツールです。wpt要素をCoursePoint要素に変換します。" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.330k.info/" />
<meta property="og:image" content="https://www.330k.info/apple-touch-icon.png"/>
<!--<meta name="viewport" content="width=640">-->
<script>
/**
 * Hubeny測地線距離計算式
 * @param {number} lat1
 * @param {number} lon1
 * @param {number} lat2
 * @param {number} lon2
 * @return {number} distance [m]
 */
const hubeny = (function(){
  const a = 6378137.0;
  const f = 1 / 298.257223563;
  const e2 = f * (2 - f);
  const degree = Math.PI / 180.0;
  const sin = Math.sin;
  const cos = Math.cos;
  const sqrt = Math.sqrt;
  return function (lat1, lon1, lat2, lon2){
    const latdiff = (lat1 - lat2) * degree;
    const londiff = (lon1 - lon2) * degree;
    const latave = 0.5 * (lat1 + lat2) * degree;
    const sinlatave = sin(latave);
    const coslatave = cos(latave);
    const w2 = 1.0 - sinlatave * sinlatave * e2;
    const w = sqrt(w2);
    const meridian = a * (1 - e2) / (w2 * w);
    const n = a / w;

    return sqrt(
      latdiff * latdiff * meridian * meridian +
      londiff * londiff * n * n * coslatave * coslatave
    );
  };
})();

/**
 * 緯度経度から地心直交座標系(X, Y, Z)に変換
 */
function latlon2XYZ(lat, lon){
  const a = 6378137.0;
  const b = 6356752.314245;
  const f2 = b * b / (a * a);
  const e2 = 1.0 - f2;
  const degree = Math.PI / 180.0;
  
  const latrad = lat * degree;
  const lonrad = lon * degree;
  
  const sinlat = Math.sin(latrad);
  const coslat = Math.cos(latrad);
  const sinlon = Math.sin(lonrad);
  const coslon = Math.cos(lonrad);
  
  const w2 = 1.0 - sinlat * sinlat * e2;
  const w = Math.sqrt(w2)
  const N = a / w;
  const h = 0.0;
  
  return {
    "X": (N + h) * coslat * coslon,
    "Y": (N + h) * coslat * sinlon,
    "Z": N * f2 * sinlat,
  };
}

/**
 * 線分と点の距離、線分上の最短距離を与える点の座標、内分比を返す(3次元)
 */
function segmentPointDistance3D(ax, ay, az, bx, by, bz, px, py, pz){
  let t = (ax * ax + ay * ay + bx * px - ax * (bx + px) + by * py - ay * (by + py) + (az - bz) * (az - pz))/((ax - bx) * (ax - bx) + (ay - by) * (ay - by) + (az - bz) * (az - bz));
  let x;
  let y;
  let z;
  
  if(t > 1){
    t = 1;
  }else if(t < 0){
    t = 0;
  }
  
  x = ax - (ax - bx) * t;
  y = ay - (ay - by) * t;
  z = az - (az - bz) * t;
  
  return {
    "dist": Math.sqrt((x - px) * (x - px) + (y - py) * (y - py) + (z - pz) * (z - pz)),
    "x": x,
    "y": y,
    "z": z,
    "t": t
  };
}

/**
 * DateをTCXファイル用のISO-8869形式の文字列に変換(秒は整数表記)
 */
function toTCXISOString(d){
  const d2 = new Date(d);
  if(d2.getMilliseconds() >= 500){
    d2.setSeconds(d2.getSeconds() + 1);
  }
  d2.setMilliseconds(0);
  
  return d2.toISOString().replace(/\.\d\d\dZ/, 'Z');
}

/**
 * GPXファイルを読み込むPromiseを返す
 * @param {string} file 読み込むファイル
 * @return {XMLDocument} 読み込んだ結果をXMLObjectを返すPromise
 */
function readGPX(file){
  return new Promise(function(resolve, reject){
    try{
      const reader = new FileReader();
      const parser = new DOMParser();

      reader.onload = function(){
        resolve(parser.parseFromString(reader.result, "text/xml"));
      };
      reader.readAsText(file, "utf-8");
      
    }catch(e){
      reject(e);
    }
  });
}

/**
 * GPXファイルをTCXに変換する
 * @param {XMLDocument} gpx
 * @param {number} speed 計算に使用する移動速度 [m/s]
 * @return {[{lat:number,lon:number,dist:number}]}
 */
function convertGPXtoTCX(gpx, speed = 15.0 / 3.6){
  const date_start = Date.now();
  const buf = [];
  
  const trkpts = gpx.querySelectorAll("trkpt");
  if(trkpts.length === 0){
    return;
  }

  // trkpts要素を列挙
  const coords = [];

  let d = 0.0;
  for(let i = 0; i < trkpts.length; i++){
    let lat = trkpts[i].getAttribute("lat") - 0;
    let lon = trkpts[i].getAttribute("lon") - 0;
    let ele = null;
    let XYZ = latlon2XYZ(lat, lon);
    
    if(trkpts[i].querySelector("ele")){
      ele = trkpts[i].querySelector("ele").textContent - 0;
    }
    if(i >= 1){
      d += hubeny(coords[coords.length - 1].lat, coords[coords.length - 1].lon, lat, lon);
    }
    coords.push({
      lat: lat,
      lon: lon,
      ele: ele,
      dist: d,
      X: XYZ.X,
      Y: XYZ.Y,
      Z: XYZ.Z,
    });
  }
  
  
  const name = gpx.querySelector("trk > name").textContent;

  buf.push('<?xml version="1.0" encoding="UTF-8"?>');
  buf.push('<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd">');
  buf.push('<Folders><Courses><CourseFolder Name="330k.info"><CourseNameRef><Id>' + name + '</Id></CourseNameRef></CourseFolder></Courses></Folders>');
  buf.push('<Courses>');
  buf.push('  <Course>');
  buf.push('    <Name>' + name + '</Name>');
  buf.push('    <Lap>');
  buf.push('      <TotalTimeSeconds>' + (d / speed).toFixed(1) + '</TotalTimeSeconds>');
  buf.push('      <DistanceMeters>' + d.toFixed(8) + '</DistanceMeters>');
  buf.push('      <BeginPosition>');
  buf.push('        <LatitudeDegrees>' + coords[0].lat.toFixed(8) + '</LatitudeDegrees>');
  buf.push('        <LongitudeDegrees>' + coords[0].lon.toFixed(8) + '</LongitudeDegrees>');
  buf.push('      </BeginPosition>');
  buf.push('      <EndPosition>');
  buf.push('        <LatitudeDegrees>' + coords[coords.length - 1].lat.toFixed(8) + '</LatitudeDegrees>');
  buf.push('        <LongitudeDegrees>' + coords[coords.length - 1].lon.toFixed(8) + '</LongitudeDegrees>');
  buf.push('      </EndPosition>');
  buf.push('      <Intensity>Active</Intensity>');
  buf.push('    </Lap>');
  buf.push('    <Track>');
  
  for(const p of coords){
    let time = new Date(date_start + (p.dist / speed * 1000));
    buf.push('      <Trackpoint>');
    buf.push('        <Time>' + toTCXISOString(time) + '</Time>');
    buf.push('        <Position>');
    buf.push('          <LatitudeDegrees>' + p.lat.toFixed(8) + '</LatitudeDegrees>');
    buf.push('          <LongitudeDegrees>' + p.lon.toFixed(8) + '</LongitudeDegrees>');
    buf.push('        </Position>');
    if(p.ele !== null){
      buf.push('        <AltitudeMeters>' + p.ele.toFixed(3) + '</AltitudeMeters>');
    }
    buf.push('        <DistanceMeters>' + p.dist.toFixed(3) + '</DistanceMeters>');
    buf.push('      </Trackpoint>');
  }
  
  buf.push('    </Track>');
  
  // wpt要素を列挙
  const wpts = gpx.querySelectorAll("wpt");
  const coursepoints = [];
  
  for(let i = 0; i < wpts.length; i++){
    let lat = wpts[i].getAttribute("lat") - 0;
    let lon = wpts[i].getAttribute("lon") - 0;
    let XYZ = latlon2XYZ(lat, lon);
    
    // コースポイントの距離を判定
    let nearest = {
      index: -1,
      t: 0,
      dist_dev: Number.MAX_VALUE
    };
    for(let j = 0; j < coords.length - 1; j++){
      let v = segmentPointDistance3D(coords[j].X, coords[j].Y, coords[j].Z, coords[j + 1].X, coords[j + 1].Y, coords[j + 1].Z, XYZ.X, XYZ.Y, XYZ.Z);
      
      if(v.dist < nearest.dist_dev){
        nearest.index = j;
        nearest.t = v.t;
        nearest.dist_dev = v.dist;
      }
    }
    console.log(nearest);
    let dist = (1 - nearest.t) * coords[nearest.index].dist + nearest.t * coords[nearest.index + 1].dist;
    let time = new Date(date_start + (dist / speed * 1000));
    
    coursepoints.push({
      name: wpts[i].querySelector("name").textContent,
      lat: lat,
      lon: lon,
      dist: dist,
      time: time
    });
  }
  
  // 距離順に並び替える(そうしないとEdgeが正しく認識しない)
  coursepoints.sort((a, b) => a.dist - b.dist);
  
  for(let i = 0; i < coursepoints.length; i++){
    buf.push('    <CoursePoint>');
    buf.push('      <Name>' + coursepoints[i].name + '</Name>');
    buf.push('      <Time>' + toTCXISOString(coursepoints[i].time) + '</Time>');
    buf.push('      <Position>');
    buf.push('        <LatitudeDegrees>' + coursepoints[i].lat.toFixed(8) + '</LatitudeDegrees>');
    buf.push('        <LongitudeDegrees>' + coursepoints[i].lon.toFixed(8) + '</LongitudeDegrees>');
    buf.push('      </Position>');
    buf.push('      <PointType>Generic</PointType>');
    buf.push('    </CoursePoint>');
  }
  
  buf.push('  </Course>');
  buf.push('</Courses>');
  buf.push('</TrainingCenterDatabase>');

  
  return buf.join("\n");
}

function downloadTCX(contents, filename){
  const e = document.createElement("a");
  let blob;
  
  try{
    blob = new Blob([contents], {type:"application/xml"});
    
  }catch(err){
    let bb = new BlobBuilder();
    bb.append(contents);
    blob = bb.getBlob("application/xml");
    
  }
  e.href = window.URL.createObjectURL(blob);
  e.setAttribute("target", "_blank");
  e.setAttribute("download", filename);
  document.body.appendChild(e);
  
  e.click();
}

window.addEventListener("DOMContentLoaded", function(){
  // GPX読み込み(基準ルート)
  document.getElementById("gpx_file").addEventListener("change", async function(evt){
    try{
      showLoader();
      const speed = (document.getElementById("input_speed").value - 0) / 3.6;

      for(const file of evt.target.files){
        const gpx = await readGPX(file);
        const tcx = convertGPXtoTCX(gpx, speed);
        downloadTCX(tcx, file.name.replace(/\.gpx/i, ".tcx"));
      }

    }catch(err){
      console.error(err);

    }finally{
      hideLoader();
      
    }
  });
  
  // ドラッグドロップ対応
  {
    const droparea = document.body;
    const overlay = document.getElementById("overlay");
    droparea.addEventListener("dragover", function(e){
      e.stopPropagation();
      e.preventDefault();
      overlay.style.display = "block";
    }, false);
    overlay.addEventListener("dragleave", function(e){
      e.stopPropagation();
      e.preventDefault();
      overlay.style.display = "none";
    }, false);
    overlay.addEventListener("drop", function(e){
      e.stopPropagation();
      e.preventDefault();
      overlay.style.display = "none";
      
      const files = e.dataTransfer.files;
      document.getElementById("gpx_file").files = files;

      const evt = new Event("change");
      document.getElementById("gpx_file").dispatchEvent(evt);
    }, false)
  };
  
  hideLoader();
});

function hideLoader(){
  document.getElementById("loader_bg").style.display = "none";
}

function showLoader(){
  document.getElementById("loader_bg").style.display = "block";
}
</script>
<link rel="stylesheet" href="https://unpkg.com/purecss@2.1.0/build/pure-min.css" integrity="<KEY>" crossorigin="anonymous">
<style>
/* * { margin: 0; padding: 0}*/
#wrapper {
  width: 100%;
  height: 100%;
  overflow: hidden;
  padding: 10px;
  border: 1px solid gray;
  border-radius: 10px;
  box-sizing: border-box;
}
#loader_bg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(1, 1, 1, 0.25);
  z-index: 9999;
}
#loader {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 10px;
  border-radius: 30px;<commit_msg>iframeリサイズ対応<commit_after>
<html>
<head>
<meta charset="utf-8">
<title>GPX to TCX Converter</title>
<meta property="og:title" content="GPX to TCX Converter" />
<meta property="og:description" content="GPXからTCXファイルに変換するツールです。wpt要素をCoursePoint要素に変換します。" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.330k.info/" />
<meta property="og:image" content="https://www.330k.info/apple-touch-icon.png"/>
<!--<meta name="viewport" content="width=640">-->
<script>
/**
 * Hubeny測地線距離計算式
 * @param {number} lat1
 * @param {number} lon1
 * @param {number} lat2
 * @param {number} lon2
 * @return {number} distance [m]
 */
const hubeny = (function(){
  const a = 6378137.0;
  const f = 1 / 298.257223563;
  const e2 = f * (2 - f);
  const degree = Math.PI / 180.0;
  const sin = Math.sin;
  const cos = Math.cos;
  const sqrt = Math.sqrt;
  return function (lat1, lon1, lat2, lon2){
    const latdiff = (lat1 - lat2) * degree;
    const londiff = (lon1 - lon2) * degree;
    const latave = 0.5 * (lat1 + lat2) * degree;
    const sinlatave = sin(latave);
    const coslatave = cos(latave);
    const w2 = 1.0 - sinlatave * sinlatave * e2;
    const w = sqrt(w2);
    const meridian = a * (1 - e2) / (w2 * w);
    const n = a / w;

    return sqrt(
      latdiff * latdiff * meridian * meridian +
      londiff * londiff * n * n * coslatave * coslatave
    );
  };
})();

/**
 * 緯度経度から地心直交座標系(X, Y, Z)に変換
 */
function latlon2XYZ(lat, lon){
  const a = 6378137.0;
  const b = 6356752.314245;
  const f2 = b * b / (a * a);
  const e2 = 1.0 - f2;
  const degree = Math.PI / 180.0;
  
  const latrad = lat * degree;
  const lonrad = lon * degree;
  
  const sinlat = Math.sin(latrad);
  const coslat = Math.cos(latrad);
  const sinlon = Math.sin(lonrad);
  const coslon = Math.cos(lonrad);
  
  const w2 = 1.0 - sinlat * sinlat * e2;
  const w = Math.sqrt(w2)
  const N = a / w;
  const h = 0.0;
  
  return {
    "X": (N + h) * coslat * coslon,
    "Y": (N + h) * coslat * sinlon,
    "Z": N * f2 * sinlat,
  };
}

/**
 * 線分と点の距離、線分上の最短距離を与える点の座標、内分比を返す(3次元)
 */
function segmentPointDistance3D(ax, ay, az, bx, by, bz, px, py, pz){
  let t = (ax * ax + ay * ay + bx * px - ax * (bx + px) + by * py - ay * (by + py) + (az - bz) * (az - pz))/((ax - bx) * (ax - bx) + (ay - by) * (ay - by) + (az - bz) * (az - bz));
  let x;
  let y;
  let z;
  
  if(t > 1){
    t = 1;
  }else if(t < 0){
    t = 0;
  }
  
  x = ax - (ax - bx) * t;
  y = ay - (ay - by) * t;
  z = az - (az - bz) * t;
  
  return {
    "dist": Math.sqrt((x - px) * (x - px) + (y - py) * (y - py) + (z - pz) * (z - pz)),
    "x": x,
    "y": y,
    "z": z,
    "t": t
  };
}

/**
 * DateをTCXファイル用のISO-8869形式の文字列に変換(秒は整数表記)
 */
function toTCXISOString(d){
  const d2 = new Date(d);
  if(d2.getMilliseconds() >= 500){
    d2.setSeconds(d2.getSeconds() + 1);
  }
  d2.setMilliseconds(0);
  
  return d2.toISOString().replace(/\.\d\d\dZ/, 'Z');
}

/**
 * GPXファイルを読み込むPromiseを返す
 * @param {string} file 読み込むファイル
 * @return {XMLDocument} 読み込んだ結果をXMLObjectを返すPromise
 */
function readGPX(file){
  return new Promise(function(resolve, reject){
    try{
      const reader = new FileReader();
      const parser = new DOMParser();

      reader.onload = function(){
        resolve(parser.parseFromString(reader.result, "text/xml"));
      };
      reader.readAsText(file, "utf-8");
      
    }catch(e){
      reject(e);
    }
  });
}

/**
 * GPXファイルをTCXに変換する
 * @param {XMLDocument} gpx
 * @param {number} speed 計算に使用する移動速度 [m/s]
 * @return {[{lat:number,lon:number,dist:number}]}
 */
function convertGPXtoTCX(gpx, speed = 15.0 / 3.6){
  const date_start = Date.now();
  const buf = [];
  
  const trkpts = gpx.querySelectorAll("trkpt");
  if(trkpts.length === 0){
    return;
  }

  // trkpts要素を列挙
  const coords = [];

  let d = 0.0;
  for(let i = 0; i < trkpts.length; i++){
    let lat = trkpts[i].getAttribute("lat") - 0;
    let lon = trkpts[i].getAttribute("lon") - 0;
    let ele = null;
    let XYZ = latlon2XYZ(lat, lon);
    
    if(trkpts[i].querySelector("ele")){
      ele = trkpts[i].querySelector("ele").textContent - 0;
    }
    if(i >= 1){
      d += hubeny(coords[coords.length - 1].lat, coords[coords.length - 1].lon, lat, lon);
    }
    coords.push({
      lat: lat,
      lon: lon,
      ele: ele,
      dist: d,
      X: XYZ.X,
      Y: XYZ.Y,
      Z: XYZ.Z,
    });
  }
  
  
  const name = gpx.querySelector("trk > name").textContent;

  buf.push('<?xml version="1.0" encoding="UTF-8"?>');
  buf.push('<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd">');
  buf.push('<Folders><Courses><CourseFolder Name="330k.info"><CourseNameRef><Id>' + name + '</Id></CourseNameRef></CourseFolder></Courses></Folders>');
  buf.push('<Courses>');
  buf.push('  <Course>');
  buf.push('    <Name>' + name + '</Name>');
  buf.push('    <Lap>');
  buf.push('      <TotalTimeSeconds>' + (d / speed).toFixed(1) + '</TotalTimeSeconds>');
  buf.push('      <DistanceMeters>' + d.toFixed(8) + '</DistanceMeters>');
  buf.push('      <BeginPosition>');
  buf.push('        <LatitudeDegrees>' + coords[0].lat.toFixed(8) + '</LatitudeDegrees>');
  buf.push('        <LongitudeDegrees>' + coords[0].lon.toFixed(8) + '</LongitudeDegrees>');
  buf.push('      </BeginPosition>');
  buf.push('      <EndPosition>');
  buf.push('        <LatitudeDegrees>' + coords[coords.length - 1].lat.toFixed(8) + '</LatitudeDegrees>');
  buf.push('        <LongitudeDegrees>' + coords[coords.length - 1].lon.toFixed(8) + '</LongitudeDegrees>');
  buf.push('      </EndPosition>');
  buf.push('      <Intensity>Active</Intensity>');
  buf.push('    </Lap>');
  buf.push('    <Track>');
  
  for(const p of coords){
    let time = new Date(date_start + (p.dist / speed * 1000));
    buf.push('      <Trackpoint>');
    buf.push('        <Time>' + toTCXISOString(time) + '</Time>');
    buf.push('        <Position>');
    buf.push('          <LatitudeDegrees>' + p.lat.toFixed(8) + '</LatitudeDegrees>');
    buf.push('          <LongitudeDegrees>' + p.lon.toFixed(8) + '</LongitudeDegrees>');
    buf.push('        </Position>');
    if(p.ele !== null){
      buf.push('        <AltitudeMeters>' + p.ele.toFixed(3) + '</AltitudeMeters>');
    }
    buf.push('        <DistanceMeters>' + p.dist.toFixed(3) + '</DistanceMeters>');
    buf.push('      </Trackpoint>');
  }
  
  buf.push('    </Track>');
  
  // wpt要素を列挙
  const wpts = gpx.querySelectorAll("wpt");
  const coursepoints = [];
  
  for(let i = 0; i < wpts.length; i++){
    let lat = wpts[i].getAttribute("lat") - 0;
    let lon = wpts[i].getAttribute("lon") - 0;
    let XYZ = latlon2XYZ(lat, lon);
    
    // コースポイントの距離を判定
    let nearest = {
      index: -1,
      t: 0,
      dist_dev: Number.MAX_VALUE
    };
    for(let j = 0; j < coords.length - 1; j++){
      let v = segmentPointDistance3D(coords[j].X, coords[j].Y, coords[j].Z, coords[j + 1].X, coords[j + 1].Y, coords[j + 1].Z, XYZ.X, XYZ.Y, XYZ.Z);
      
      if(v.dist < nearest.dist_dev){
        nearest.index = j;
        nearest.t = v.t;
        nearest.dist_dev = v.dist;
      }
    }
    console.log(nearest);
    let dist = (1 - nearest.t) * coords[nearest.index].dist + nearest.t * coords[nearest.index + 1].dist;
    let time = new Date(date_start + (dist / speed * 1000));
    
    coursepoints.push({
      name: wpts[i].querySelector("name").textContent,
      lat: lat,
      lon: lon,
      dist: dist,
      time: time
    });
  }
  
  // 距離順に並び替える(そうしないとEdgeが正しく認識しない)
  coursepoints.sort((a, b) => a.dist - b.dist);
  
  for(let i = 0; i < coursepoints.length; i++){
    buf.push('    <CoursePoint>');
    buf.push('      <Name>' + coursepoints[i].name + '</Name>');
    buf.push('      <Time>' + toTCXISOString(coursepoints[i].time) + '</Time>');
    buf.push('      <Position>');
    buf.push('        <LatitudeDegrees>' + coursepoints[i].lat.toFixed(8) + '</LatitudeDegrees>');
    buf.push('        <LongitudeDegrees>' + coursepoints[i].lon.toFixed(8) + '</LongitudeDegrees>');
    buf.push('      </Position>');
    buf.push('      <PointType>Generic</PointType>');
    buf.push('    </CoursePoint>');
  }
  
  buf.push('  </Course>');
  buf.push('</Courses>');
  buf.push('</TrainingCenterDatabase>');

  
  return buf.join("\n");
}

function downloadTCX(contents, filename){
  const e = document.createElement("a");
  let blob;
  
  try{
    blob = new Blob([contents], {type:"application/xml"});
    
  }catch(err){
    let bb = new BlobBuilder();
    bb.append(contents);
    blob = bb.getBlob("application/xml");
    
  }
  e.href = window.URL.createObjectURL(blob);
  e.setAttribute("target", "_blank");
  e.setAttribute("download", filename);
  document.body.appendChild(e);
  
  e.click();
}

window.addEventListener("DOMContentLoaded", function(){
  // GPX読み込み(基準ルート)
  document.getElementById("gpx_file").addEventListener("change", async function(evt){
    try{
      showLoader();
      const speed = (document.getElementById("input_speed").value - 0) / 3.6;

      for(const file of evt.target.files){
        const gpx = await readGPX(file);
        const tcx = convertGPXtoTCX(gpx, speed);
        downloadTCX(tcx, file.name.replace(/\.gpx/i, ".tcx"));
      }

    }catch(err){
      console.error(err);

    }finally{
      hideLoader();
      
    }
  });
  
  // ドラッグドロップ対応
  {
    const droparea = document.body;
    const overlay = document.getElementById("overlay");
    droparea.addEventListener("dragover", function(e){
      e.stopPropagation();
      e.preventDefault();
      overlay.style.display = "block";
    }, false);
    overlay.addEventListener("dragleave", function(e){
      e.stopPropagation();
      e.preventDefault();
      overlay.style.display = "none";
    }, false);
    overlay.addEventListener("drop", function(e){
      e.stopPropagation();
      e.preventDefault();
      overlay.style.display = "none";
      
      const files = e.dataTransfer.files;
      document.getElementById("gpx_file").files = files;

      const evt = new Event("change");
      document.getElementById("gpx_file").dispatchEvent(evt);
    }, false)
  };
  
  hideLoader();
});

function hideLoader(){
  document.getElementById("loader_bg").style.display = "none";
}

function showLoader(){
  document.getElementById("loader_bg").style.display = "block";
}

/**
 * iframeで埋め込まれたときに親ウィンドウに自身の高さ情報を渡し、リサイズを要求
 */
function notifyResize(){
  const target = parent.postMessage ? parent : (parent.document.postMessage ? parent.document : undefined);
  if (typeof target !== "undefined") {
    target.postMessage(JSON.stringify({
      "message": "resize",
      "height": document.body.scrollHeight
    }), "*");
  }
}
</script>
<link rel="stylesheet" href="https://unpkg.com/purecss@2.1.0/build/pure-min.css" integrity="<KEY>" crossorigin="anonymous">
<style>
/* * { margin: 0; padding: 0}*/
#wrapper {
  /*width: 100%;
  height: 100%;*/
  overflow: hidden;
  padding: 10px;
  border: 1px solid gray;
  border-radius: 10px;
  box-sizing: border-box;
}
#loader_bg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(1, 1, 1, 0.25);
  z-index: 9999;
}
#loader {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 10px;
  border-radius: 30px;
{'fraction_non_alphanumeric': 0.19026177548227607, 'fraction_numerical': 0.021277534984328786, 'mean_word_length': 2.83381282788966, 'pattern_counts': {'":': 18, '<': 187, '<?xml version=': 2, '>': 185, 'https://': 6, 'lorem ipsum': 0, 'www.': 12, 'xml': 18}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4973753', 'n_tokens_mistral': 9131, 'n_tokens_neox': 8841, 'n_words': 1706}