{"id":13,"date":"2026-10-25T22:43:40","date_gmt":"2026-10-25T22:43:40","guid":{"rendered":"https:\/\/blog.passagemail.com\/ride-with-chris\/home\/"},"modified":"2026-11-04T15:56:35","modified_gmt":"2026-11-04T15:56:35","slug":"home","status":"publish","type":"page","link":"https:\/\/blog.passagemail.com\/ride-with-chris\/","title":{"rendered":"Home"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Ride With Chris<\/h1>\n\n\n\n\n\n<h2 class=\"wp-block-heading\">Voyage Tracker<\/h2>\n\n\n    <div class=\"pm-tracker-block\" style=\"margin:1em 0\">\n            <style>.pm-mapmatch-controls,.pm-fix-controls{padding:8px 12px;background:#fff;color:#0b1a2b;font:13px\/1.4 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif}.pm-fix-controls select,.pm-match-row select{margin-left:6px;max-width:100%;padding:4px}.pm-fix-controls span{color:#607181}.pm-match-row{display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;margin-top:5px}.pm-match-status{color:#607181}.pm-poi-marker{display:grid;place-items:center;width:32px;height:32px;padding:0;border:2px solid #fff;border-radius:50%;background:#7c3aed;color:#fff;font:20px sans-serif;box-shadow:0 1px 5px #0006;cursor:pointer;overflow:hidden}.pm-poi-marker img{width:100%;height:100%;object-fit:cover}.pm-poi-icon{border:0;background:none}.pm-poi-post{max-width:280px;max-height:300px;overflow:auto}.pm-poi-post img{max-width:100%;height:auto}<\/style>\n      <div id=\"pm-tr-d698b6a4-fix\" class=\"pm-fix-controls\"><\/div>\n      <div id=\"pm-tr-d698b6a4-match\" class=\"pm-mapmatch-controls\"><\/div>\n      <div id=\"pm-tr-d698b6a4-map\" style=\"height:420px;min-height:300px;width:100%;border-radius:6px;overflow:hidden\"><\/div>\n      <div id=\"pm-tr-d698b6a4-sum\" style=\"padding:8px 12px;background:#fff;border:1px solid #e2e8ee;border-top:none;border-radius:0 0 6px 6px;color:#607181;font-size:13px;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap\">\n        <span><strong style=\"color:#0b1a2b\">Positions:<\/strong> <span class=\"pm-count\">Loading\u2026<\/span><\/span>\n        <span><strong style=\"color:#0b1a2b\">Latest:<\/strong> <span class=\"pm-latest\">\u2014<\/span><\/span>\n        <span><a href=\"https:\/\/passagemail.com\/\" style=\"color:#e27600\" target=\"_blank\" rel=\"noopener\">PassageMail<\/a><\/span>\n      <\/div>\n    <\/div>\n    <script src=\"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-content\/plugins\/pm-tracker\/assets\/map-view.js?v=20261111-4\"><\/script>\n    <script src=\"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-content\/plugins\/pm-tracker\/assets\/poi.js?v=20261028-4\"><\/script>\n    <script src=\"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-content\/plugins\/pm-tracker\/assets\/mapmatch-owner-route-v1.js?v=20261028-3\"><\/script>\n    <script>\n    window.pmTrackerLoadFeed = window.pmTrackerLoadFeed || async function (seed) {\n  if (seed.owner_preview === true) return seed;\n  const seen = new Set(), tracks = new Map();\n  const positions = [];\n  let latest = seed.latest_at_iso, cursor = seed.next_cursor;\n  function append(page, slug) {\n    if (slug !== seed.slug || !Array.isArray(page.positions) || !Array.isArray(page.tracks)) {\n      throw new Error('Tracker response unavailable');\n    }\n    page.positions.forEach(p => positions.push(p));\n    page.tracks.forEach(t => tracks.set(t.track, t));\n    if ((page.latest_at_iso || '') > (latest || '')) latest = page.latest_at_iso;\n  }\n  try {\n    append(seed, seed.slug);\n    while (cursor !== null && cursor !== '') {\n      if (typeof cursor !== 'string' || seen.has(cursor)) throw new Error('Tracker cursor unavailable');\n      seen.add(cursor);\n      const controller = new AbortController();\n      \/\/ Regular browser HTTP only. No limit on page count or total positions.\n      const timer = setTimeout(() => controller.abort(), 10000);\n      let page;\n      try {\n        const url = seed.api + '\/positions?u=' + encodeURIComponent(seed.slug) + '&cursor=' + encodeURIComponent(cursor);\n        const response = await fetch(url, {\n          credentials: 'omit', redirect: 'error', cache: 'no-store', signal: controller.signal,\n          headers: {Accept: 'application\/json'}\n        });\n        if (response.status !== 200) throw new Error('Tracker unavailable');\n        page = await response.json();\n      } finally { clearTimeout(timer); }\n      append(page, page.blog_slug);\n      cursor = page.next_cursor === undefined ? null : page.next_cursor;\n    }\n    return Object.assign({}, seed, {positions, tracks: Array.from(tracks.values()), latest_at_iso: latest, next_cursor: null});\n  } catch (error) {\n    \/\/ Do not render a partial voyage as complete, empty, or still public after\n    \/\/ a mid-chain visibility change. Callers show an explicit unavailable state.\n    seed.positions = []; seed.tracks = []; seed.latest_at_iso = null;\n    positions.length = 0; tracks.clear();\n    throw error;\n  }\n};    (function () {\n      var RENDERER = \"leaflet\";\n      var MAP_ID = 'pm-tr-d698b6a4-map';\n      var SUM_ID = 'pm-tr-d698b6a4-sum';\n      var FIT_MODE = \"\";\n      function updateSum(PM, totalCount) {\n        var sum = document.getElementById(SUM_ID);\n        if (!sum) return;\n        sum.querySelector('.pm-count').textContent = totalCount;\n        sum.querySelector('.pm-latest').textContent = PM.latest_at_iso\n          ? window.pmTrackerFormatTime(PM.latest_at_iso) : '\u2014';\n      }\n\n      async function drawTracker(PM) {\n        var view = await window.pmTrackerCreateMap({ container: MAP_ID, slug: PM.slug, renderer: PM.slug === 'captruss' ? 'leaflet' : RENDERER, voyages: PM.voyages, tracks: PM.tracks });\n        var map = view.map;\n        view.setupFixControls('pm-tr-d698b6a4-fix');\n        if (typeof window.pmTrackerSetupPOI === 'function' && !PM.owner_preview) {\n          window.pmTrackerSetupPOI({ view: view, slug: PM.slug });\n        }\n        \/\/ GO reports from Mongo\n        var pts = (PM.positions || []).map(function (p) { return [p.lat, p.lon]; });\n        \/\/ App voyages from SQL \u2014 each voyage gets its own polyline.\n        var voyages = (PM.voyages || []).map(function (v) { return PM.slug === 'captruss' ? Object.assign({}, v, {color: '#2780AB'}) : v; });\n        if (PM.slug === 'captruss') {\n          view.legend();\n          view.trackDetails(view.latestPoint(PM.positions || []), view.latestPoint(voyages.flatMap(v => v.positions || [])));\n        }\n        var allPoints = pts.slice();\n        var voyagePolys = [];\n        var voyagePolyById = {};\n        voyages.forEach(function (v) {\n          var vpts = (v.positions || []).map(function (p) { return [p.lat, p.lon]; });\n          if (vpts.length) {\n            var color = v.color || '#2780AB';\n            var vpoly = view.line(vpts, { color: color, weight: 3, opacity: 0.85 }).addTo(map);\n            vpoly.bindTooltip(v.name || 'Track', { sticky: true });\n            voyagePolys.push(vpoly);\n            voyagePolyById[v.client_voyage_id] = vpoly;\n            allPoints = allPoints.concat(vpts);\n          }\n        });\n        view.points(PM.positions || [], 'Iridium GO position', '#e27600');\n        voyages.forEach(function (v) { view.points(v.positions || [], v.name || 'App position', v.color || '#2780AB'); });\n        if (typeof window.pmTrackerSetupMatching === 'function') {\n          window.pmTrackerSetupMatching({ view: view, map: map, voyages: voyages, polylines: voyagePolyById,\n            controlId: 'pm-tr-d698b6a4-match', slug: PM.slug, api: PM.api });\n        }\n        var totalCount = pts.length + voyages.reduce(function (a, v) { return a + (v.positions_count || (v.positions || []).length); }, 0);\n        var sum = document.getElementById('pm-tr-d698b6a4-sum');\n        sum.querySelector('.pm-count').textContent = totalCount;\n        sum.querySelector('.pm-latest').textContent = PM.latest_at_iso\n          ? view.formatTime(PM.latest_at_iso) : '\u2014';\n        if (allPoints.length === 0) { view.fit([], FIT_MODE); return; }\n        var goPoly = pts.length ? view.line(pts, { color: '#e27600', weight: 3, opacity: 0.85 }).addTo(map) : null;\n        \/\/ Marker on the newest position across all sources.\n        var latestPt = null, latestMs = -1;\n        (PM.positions || []).forEach(function (p) {\n          var ms = p.fix_at_iso ? Date.parse(p.fix_at_iso) : (p.fix_at_ms || 0);\n          if (ms > latestMs) { latestMs = ms; latestPt = p; }\n        });\n        voyages.forEach(function (v) {\n          (v.positions || []).forEach(function (p) {\n            if ((p.fix_at_ms || 0) > latestMs) { latestMs = p.fix_at_ms; latestPt = p; }\n          });\n        });\n        if (latestPt) view.marker([latestPt.lat, latestPt.lon]).bindPopup(view.positionDetails(latestPt, 'Latest position')).addTo(map);\n        view.fit(allPoints, FIT_MODE);\n      }\n\n      function boot() {\n        window.pmTrackerLoadFeed({\"slug\":\"ride-with-chris\",\"tracker_name\":\"Ride With Chris\",\"vessel_name\":\"Harley Davidson 2019 FLHT\",\"latest_at_iso\":\"2026-11-10T20:15:14Z\",\"tracks\":[],\"positions\":[],\"api\":\"https:\/\/passagemail.net\/api\/tracker\",\"next_cursor\":null,\"owner_preview\":false,\"voyages\":[{\"client_voyage_id\":\"b25b2f7e-4d3a-4c62-a25f-134963057849\",\"name\":\"VC Loop\",\"color\":\"#2780AB\",\"category_id\":\"motorcycle\",\"category_label\":\"Motorcycle\",\"visibility\":\"public\",\"started_at_ms\":1794341711760,\"ended_at_ms\":1794341738794,\"public_delay_seconds\":0,\"positions\":[{\"client_pos_id\":\"29075a49-8402-8cde-be13-ede7b6aefab7\",\"fix_at_ms\":1794341714680,\"lat\":39.49399073000000015554178389720618724822998046875,\"lon\":-119.8318661699999978509367792867124080657958984375,\"speed_mps\":1.230000019073500094890505351941101253032684326171875,\"course_deg\":284,\"altitude_m\":1401.58178710940001110429875552654266357421875,\"first_public_at_ms\":1794342003000}],\"positions_count\":1}]}).then(drawTracker).catch(function () {\n          var m = document.getElementById(MAP_ID); if (m) m.textContent = 'Tracker temporarily unavailable. Please try again.';\n          var s = document.getElementById(SUM_ID); if (s) s.textContent = 'Positions unavailable';\n        });\n      }\n      if (document.readyState === 'complete') boot();\n      else window.addEventListener('load', boot, { once: true });\n    })();\n    <\/script>\n    \n\n\n\n<h2 class=\"wp-block-heading\">Latest Posts<\/h2>\n\n\n\n<div class=\"wp-block-query is-layout-flow wp-block-query-is-layout-flow\"><\/div>\n\n\n","protected":false},"excerpt":{"rendered":"<p>Ride With Chris Voyage Tracker Latest Posts<\/p>\n","protected":false},"author":86,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-13","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/pages\/13","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/users\/86"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/comments?post=13"}],"version-history":[{"count":4,"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/pages\/13\/revisions"}],"predecessor-version":[{"id":27,"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/pages\/13\/revisions\/27"}],"wp:attachment":[{"href":"https:\/\/blog.passagemail.com\/ride-with-chris\/wp-json\/wp\/v2\/media?parent=13"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}