{"id":54,"date":"2025-12-29T21:33:59","date_gmt":"2025-12-29T21:33:59","guid":{"rendered":"https:\/\/www.martinmartin.nl\/?page_id=54"},"modified":"2026-02-06T22:34:39","modified_gmt":"2026-02-06T21:34:39","slug":"volg-ons-live","status":"publish","type":"page","link":"https:\/\/www.martinmartin.nl\/?page_id=54","title":{"rendered":"Volg ons Live"},"content":{"rendered":"\n<p class=\"has-text-align-center\"><\/p>\n\n\n\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\"><div id=\"traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673\" class=\"mm-km-wrap\">\n  <div class=\"mm-km-label\">Gereden kilometers vandaag<\/div>\n\n  <div class=\"mm-km-row\">\n    <div class=\"mm-tile\" data-idx=\"0\">0<\/div><div class=\"mm-tile\" data-idx=\"1\">0<\/div><div class=\"mm-tile\" data-idx=\"2\">0<\/div><div class=\"mm-tile\" data-idx=\"3\">0<\/div><div class=\"mm-dot\">.<\/div><div class=\"mm-tile\" data-idx=\"4\">0<\/div>    <div class=\"mm-km-unit\">km<\/div>\n  <\/div>\n\n  <\/div>\n\n<style>\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673.mm-km-wrap{\n  display:inline-block; padding:14px 16px; border:1px solid #e5e5e5; border-radius:14px; background:#fff;\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-km-label{\n  font:600 14px\/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial; margin-bottom:10px;\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-km-row{\n  display:flex; gap:6px; align-items:baseline; flex-wrap:wrap;\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-tile{\n  width:30px; height:44px;\n  border-radius:12px;\n  display:flex; align-items:center; justify-content:center;\n  background:#111; color:#fff;\n  font:800 28px\/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;\n  box-shadow: inset 0 -2px 0 rgba(255,255,255,.06);\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-dot{\n  width:10px; height:44px;\n  display:flex; align-items:center; justify-content:center;\n  font:900 24px\/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;\n  color:#111;\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-km-unit{\n  margin-left:10px;\n  font:700 14px\/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;\n  color:#333;\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-km-debug{\n  margin-top:10px;\n  font:12px\/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n  white-space:pre-wrap;\n}\n#traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673 .mm-pop{ transform: scale(1.08); }\n<\/style>\n\n<script>\n(function(){\n  const wrap = document.getElementById(\"traccarKmWrap_a523bd6f-ba18-4308-b9dd-7171e82c4673\");\n  if(!wrap) return;\n\n  const tiles = Array.from(wrap.querySelectorAll(\".mm-tile\"));\n  tiles.forEach((t, i) => {\n    setTimeout(() => {\n      t.classList.add(\"mm-pop\");\n      setTimeout(() => t.classList.remove(\"mm-pop\"), 120);\n    }, i * 70);\n  });\n})();\n<\/script>\n\n\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\"><\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\"><div id=\"traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138\" class=\"mm-km-wrap\">\n  <div class=\"mm-km-label\">Totaal kilometers gereden<\/div>\n\n  <div class=\"mm-km-row\">\n    <div class=\"mm-tile\" data-idx=\"0\">5<\/div><div class=\"mm-tile\" data-idx=\"1\">7<\/div><div class=\"mm-tile\" data-idx=\"2\">4<\/div><div class=\"mm-tile\" data-idx=\"3\">9<\/div><div class=\"mm-dot\">.<\/div><div class=\"mm-tile\" data-idx=\"4\">9<\/div>    <div class=\"mm-km-unit\">km<\/div>\n  <\/div>\n\n  <\/div>\n\n<style>\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138.mm-km-wrap{\n  display:inline-block; padding:14px 16px; border:1px solid #e5e5e5; border-radius:14px; background:#fff;\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-km-label{\n  font:600 14px\/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial; margin-bottom:10px;\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-km-row{\n  display:flex; gap:6px; align-items:baseline; flex-wrap:wrap;\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-tile{\n  width:30px; height:44px;\n  border-radius:12px;\n  display:flex; align-items:center; justify-content:center;\n  background:#111; color:#fff;\n  font:800 28px\/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;\n  box-shadow: inset 0 -2px 0 rgba(255,255,255,.06);\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-dot{\n  width:10px; height:44px;\n  display:flex; align-items:center; justify-content:center;\n  font:900 24px\/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;\n  color:#111;\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-km-unit{\n  margin-left:10px;\n  font:700 14px\/1 system-ui,-apple-system,Segoe UI,Roboto,Arial;\n  color:#333;\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-km-debug{\n  margin-top:10px;\n  font:12px\/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\n  white-space:pre-wrap;\n}\n#traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138 .mm-pop{ transform: scale(1.08); }\n<\/style>\n\n<script>\n(function(){\n  const wrap = document.getElementById(\"traccarKmWrap_bbdccc24-946e-4689-a3ae-5fd6c6ac9138\");\n  if(!wrap) return;\n\n  const tiles = Array.from(wrap.querySelectorAll(\".mm-tile\"));\n  tiles.forEach((t, i) => {\n    setTimeout(() => {\n      t.classList.add(\"mm-pop\");\n      setTimeout(() => t.classList.remove(\"mm-pop\"), 120);\n    }, i * 70);\n  });\n})();\n<\/script>\n\n\n<\/div>\n<\/div>\n\n\n<link rel=\"stylesheet\" href=\"https:\/\/unpkg.com\/leaflet@1.9.4\/dist\/leaflet.css\">\n<div id=\"traccarMap_3_7a664319-3e11-4228-b546-0ba1399f3cde\" style=\"height: 600px; width:100%; border-radius:12px; overflow:hidden; position:relative;\"><\/div>\n<div id=\"traccarRefresh_3_2f00f81f-5140-4e12-974d-b9a0fcaa2e8a\" style=\"margin-top:8px; font: 13px\/1.4 system-ui,-apple-system,Segoe UI,Roboto,Arial; color:#333;\">\n  Live: \u2014 | Route: \u2014\n<\/div>\n\n<script src=\"https:\/\/unpkg.com\/leaflet@1.9.4\/dist\/leaflet.js\"><\/script>\n<script>\n(function(){\n  const liveUrl  = \"https:\\\/\\\/www.martinmartin.nl\\\/?rest_route=\\\/traccar\\\/v1\\\/live&deviceid=3\";\n  const routeUrl = \"https:\\\/\\\/www.martinmartin.nl\\\/?rest_route=\\\/traccar\\\/v1\\\/route&deviceid=3\";\n\n  const refreshLiveSec = 5;\n  const moveThresholdM = 25;\n  const routeMinIntervalSec = 60;\n  const routeOnLoad = true;\n  const defaultZoom = 15;\n  const debugOn = false;\n\n  const infoEl = document.getElementById(\"traccarRefresh_3_2f00f81f-5140-4e12-974d-b9a0fcaa2e8a\");\n  const dbgEl  = debugOn ? document.getElementById(\"traccarDebug_3_6f0bf6bd-f735-41a0-a4cd-01a78f3f383e\") : null;\n  const mapEl  = document.getElementById(\"traccarMap_3_7a664319-3e11-4228-b546-0ba1399f3cde\");\n\n  const fmt = new Intl.DateTimeFormat(\"nl-NL\", { timeZone:\"Europe\/Amsterdam\", hour:\"2-digit\", minute:\"2-digit\", second:\"2-digit\" });\n\n  let lastLiveRefreshDate=null, lastRouteRefreshDate=null;\n  function updateRefreshLabel(){\n    infoEl.textContent = \"Live: \" + (lastLiveRefreshDate?fmt.format(lastLiveRefreshDate):\"\u2014\") +\n                         \" | Route: \" + (lastRouteRefreshDate?fmt.format(lastRouteRefreshDate):\"\u2014\");\n  }\n  function setDbg(s){ if(dbgEl) dbgEl.textContent = \"debug:\\n\" + s; }\n\n  const map = L.map(mapEl, { preferCanvas: true }).setView([52.37, 4.90], 12);\n  L.tileLayer(\"https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png\", { maxZoom: 19 }).addTo(map);\n\n  map.createPane(\"traccarRoutePane\");\n  map.getPane(\"traccarRoutePane\").style.zIndex = 650;\n  const canvasRenderer = L.canvas({ pane: \"traccarRoutePane\" });\n\n  let marker=null, line=null;\n  let lastLiveOk=false, lastLiveLat=null, lastLiveLon=null;\n  let lastRouteFetchTs=0, lastRouteAnchor=null, routeInFlight=false;\n  let lastLatLngs=null;\n\n  function mapHasRealSize(){\n    try{\n      const s = map.getSize && map.getSize();\n      return !!(s && s.x > 10 && s.y > 10);\n    } catch(e){\n      return false;\n    }\n  }\n\n  function safeInvalidate(){\n    try { map.invalidateSize(true); } catch(e) {}\n  }\n\n  \/\/ Wait until the map has a real size before drawing route\n  function whenMapReadyDraw(cb, attempts=40){\n    \/\/ attempts * 250ms = 10s max\n    const tick = () => {\n      safeInvalidate();\n      if (mapHasRealSize()) return cb();\n      if (attempts <= 0) return;\n      setTimeout(() => whenMapReadyDraw(cb, attempts-1), 250);\n    };\n    tick();\n  }\n\n  function drawRouteNow(){\n    if(!lastLatLngs || lastLatLngs.length < 2) return;\n\n    try { if(line) map.removeLayer(line); } catch(e) {}\n    try{\n      line = L.polyline(lastLatLngs, {\n        pane: \"traccarRoutePane\",\n        renderer: canvasRenderer,\n        color: \"#ff0000\",\n        weight: 5,\n        opacity: 1\n      }).addTo(map);\n      setDbg((debugOn ? (\"route points: \" + lastLatLngs.length + \"\\nmap size ok: \" + mapHasRealSize()) : \"\"));\n    } catch(e){\n      \/\/ This is where your \"reading 'x'\" happens -> retry later\n      setDbg(\"drawRoute error: \" + (e && e.message ? e.message : String(e)) + \"\\nRetrying...\");\n      setTimeout(() => whenMapReadyDraw(drawRouteNow, 40), 300);\n    }\n  }\n\n  \/\/ Visibility hook (tabs\/accordions\/page builders)\n  const io = new IntersectionObserver((entries) => {\n    for (const e of entries) {\n      if (e.isIntersecting) {\n        safeInvalidate();\n        if(lastLatLngs && lastLatLngs.length > 1){\n          whenMapReadyDraw(drawRouteNow, 40);\n        }\n      }\n    }\n  }, { threshold: 0.1 });\n  io.observe(mapEl);\n\n  \/\/ extra invalidations after load\n  window.addEventListener(\"load\", () => {\n    safeInvalidate();\n    setTimeout(safeInvalidate, 250);\n    setTimeout(safeInvalidate, 1000);\n    setTimeout(() => { safeInvalidate(); if(lastLatLngs) whenMapReadyDraw(drawRouteNow, 40); }, 2000);\n  });\n\n  function haversineMeters(lat1, lon1, lat2, lon2){\n    const R=6371000, toRad=x=>x*Math.PI\/180;\n    const dLat=toRad(lat2-lat1), dLon=toRad(lon2-lon1);\n    const a=Math.sin(dLat\/2)**2 + Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon\/2)**2;\n    return 2*R*Math.asin(Math.sqrt(a));\n  }\n\n  async function fetchLive(){\n    try{\n      const res = await fetch(liveUrl, { cache:\"no-store\" });\n      const txt = await res.text();\n      lastLiveRefreshDate = new Date(); updateRefreshLabel();\n      if(!res.ok){ lastLiveOk=false; return; }\n\n      const data = JSON.parse(txt);\n      const pos = data.position || null;\n\n      lastLiveOk=false;\n      if(pos && pos.latitude != null && pos.longitude != null){\n        const lat = Number(pos.latitude), lon = Number(pos.longitude);\n        if(!Number.isNaN(lat) && !Number.isNaN(lon)){\n          lastLiveOk=true; lastLiveLat=lat; lastLiveLon=lon;\n\n          const latlng=[lat,lon];\n          if(!marker){\n            marker=L.marker(latlng).addTo(map);\n            whenMapReadyDraw(() => { map.setView(latlng, defaultZoom); }, 40);\n          } else {\n            marker.setLatLng(latlng);\n          }\n        }\n      }\n\n      const now = Date.now();\n      const neverFetched = lastLiveOk && lastRouteFetchTs===0;\n      const dueByTime = lastLiveOk && (now-lastRouteFetchTs) > routeMinIntervalSec*1000;\n\n      let dueByMove=false;\n      if(lastLiveOk && lastRouteAnchor){\n        dueByMove = haversineMeters(lastRouteAnchor.lat,lastRouteAnchor.lon,lastLiveLat,lastLiveLon) >= moveThresholdM;\n      }\n      if(neverFetched || dueByMove || dueByTime) fetchRoute();\n\n    } catch(e){}\n  }\n\n  async function fetchRoute(){\n    if(routeInFlight) return;\n    routeInFlight=true;\n\n    try{\n      const res = await fetch(routeUrl, { cache:\"no-store\" });\n      const txt = await res.text();\n      lastRouteRefreshDate = new Date(); updateRefreshLabel();\n\n      if(!res.ok){\n        setDbg(\"route http \" + res.status + \"\\n\" + txt.slice(0,200));\n        routeInFlight=false; return;\n      }\n\n      const data = JSON.parse(txt);\n      const route = Array.isArray(data.route) ? data.route : [];\n\n      lastLatLngs = route\n        .map(p => [Number(p.latitude), Number(p.longitude)])\n        .filter(ll => !Number.isNaN(ll[0]) && !Number.isNaN(ll[1]));\n\n      if(debugOn) setDbg(\"route points: \" + lastLatLngs.length + \"\\nmapHasRealSize: \" + mapHasRealSize());\n\n      if(lastLatLngs.length > 1){\n        whenMapReadyDraw(drawRouteNow, 40);\n      }\n\n      lastRouteFetchTs = Date.now();\n      if(lastLiveOk) lastRouteAnchor = { lat:lastLiveLat, lon:lastLiveLon };\n\n    } catch(e){\n      setDbg(\"route exception: \" + (e && e.message ? e.message : String(e)));\n    } finally {\n      routeInFlight=false;\n    }\n  }\n\n  updateRefreshLabel();\n  if(routeOnLoad) fetchRoute();\n  fetchLive();\n  setInterval(fetchLive, refreshLiveSec*1000);\n})();\n<\/script>\n\n\n\n\n<figure class=\"wp-block-image aligncenter size-full is-style-default\" style=\"margin-right:0;margin-left:0\"><img loading=\"lazy\" decoding=\"async\" width=\"853\" height=\"480\" src=\"https:\/\/www.martinmartin.nl\/wp-content\/uploads\/2025\/12\/ee5762ef-fc2c-46bf-a0fe-fa92355540c3-e1767071883513.png\" alt=\"\" class=\"wp-image-81\" srcset=\"https:\/\/www.martinmartin.nl\/wp-content\/uploads\/2025\/12\/ee5762ef-fc2c-46bf-a0fe-fa92355540c3-e1767071883513.png 853w, https:\/\/www.martinmartin.nl\/wp-content\/uploads\/2025\/12\/ee5762ef-fc2c-46bf-a0fe-fa92355540c3-e1767071883513-300x169.png 300w, https:\/\/www.martinmartin.nl\/wp-content\/uploads\/2025\/12\/ee5762ef-fc2c-46bf-a0fe-fa92355540c3-e1767071883513-768x432.png 768w\" sizes=\"auto, (max-width: 853px) 100vw, 853px\" \/><\/figure>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-54","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=\/wp\/v2\/pages\/54","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=54"}],"version-history":[{"count":5,"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=\/wp\/v2\/pages\/54\/revisions"}],"predecessor-version":[{"id":398,"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=\/wp\/v2\/pages\/54\/revisions\/398"}],"wp:attachment":[{"href":"https:\/\/www.martinmartin.nl\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=54"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}