-
ELR Fare Price
-
$0
-
Enter miles or select a destination to see pricing.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+// Utils
+function roundToFiveMinutes(d){ const ms=1000*60*5; return new Date(Math.round(d.getTime()/ms)*ms); }
+function addHours(d,h){ return new Date(d.getTime()+h*60*60*1000); }
+function toLocalInput(d){
+ const pad=n=>String(n).padStart(2,"0");
+ return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
+
+function normalizeLocationText(text){
+ if(!text) return text;
+ let t=text.replace(/,\s*(United States|USA|United States of America)\.?$/i,"");
+ t=t.replace(/\bNew Jersey\b/gi,"NJ");
+ return t.trim();
+}
+
+function abbreviateLocation(text){
+ if(!text) return text;
+ const map={
+ "Avenue":"Ave",
+ "Street":"St",
+ "Road":"Rd",
+ "Drive":"Dr",
+ "Lane":"Ln",
+ "Court":"Ct",
+ "Boulevard":"Blvd",
+ "Parkway":"Pkwy",
+ "Highway":"Hwy",
+ "West":"W",
+ "East":"E",
+ "North":"N",
+ "South":"S"
+ };
+ const pattern=new RegExp(`\\b(${Object.keys(map).join("|")})\\b`,"gi");
+ return text.replace(pattern,(m)=>map[m.charAt(0).toUpperCase()+m.slice(1).toLowerCase()]||m);
+}
+
+function formatSuggestionLabel(item, selectionValue, headline){
+ const addr = item.full_address?.split(",")[0]?.trim() || item.name || item.context?.address?.name || headline;
+ const city=item.context?.place?.name;
+ const state=normalizeLocationText(item.context?.region?.abbr || item.context?.region?.name || "");
+ const tail=[city,state].filter(Boolean).join(", ");
+ const labelParts=[];
+ if(addr) labelParts.push(addr);
+ if(tail) labelParts.push(tail);
+ const label=abbreviateLocation(labelParts.join(" ").trim());
+ return label || selectionValue || headline || "Unknown";
+}
+
+function calcFare(miles){
+ if(miles<1)return 5;
+ if(miles<=10)return Math.round(5+miles);
+ if(miles<=15)return Math.round(5+miles*1.1);
+ if(miles<=20)return Math.round(5+miles*1.2);
+ return Math.round(5+miles*1.3);
+}
+function fareFormulaText(miles){
+ if(miles<=10)return"Fare = $5 + miles (rounded)";
+ if(miles<=15)return"Fare = $5 + (miles × 1.1), rounded";
+ if(miles<=20)return"Fare = $5 + (miles × 1.2), rounded";
+ return"Fare = $5 + (miles × 1.3), rounded";
+}
+
+function showChip(field,text){
+ field.chipText.textContent=text;
+ field.chip.style.display="inline-flex";
+ field.chipWrapper.style.display="block";
+ field.input.classList.add("has-inline-chip");
+ field.input.dataset.selected=text;
+ field.input.value="";
+ field.input.placeholder="";
+}
+function hideChip(field){
+ field.chip.style.display="none";
+ field.chipText.textContent="";
+ field.chipWrapper.style.display="none";
+ field.input.classList.remove("has-inline-chip");
+ delete field.input.dataset.selected;
+ clearSelectedPlace(field.key);
+}
+
+function clearSelectedPlace(key){
+ selectedPlaces[key]=null;
+ updateDistanceUI();
+ updateSwapState();
+}
+function setSelectedPlace(key,data){
+ selectedPlaces[key]=data;
+ updateDistanceUI();
+ maybeComputeDistance();
+ updateSwapState();
+}
+
+function updateDistanceUI(opts={}){
+ if(opts.error){
+ distanceValue.textContent="--";
+ if(distanceNote) distanceNote.textContent=opts.error;
+ updatePrice({error:opts.error});
+ return;
+ }
+ if(opts.loading){
+ distanceValue.textContent="...";
+ if(distanceNote) distanceNote.textContent="Calculating driving distance...";
+ updatePrice({loading:true});
+ return;
+ }
+ if(typeof opts.miles==="number"){
+ distanceValue.textContent=opts.miles.toFixed(1);
+ if(distanceNote) distanceNote.textContent="";
+ updatePrice({miles:opts.miles});
+ return;
+ }
+ if(selectedPlaces.from && selectedPlaces.to){
+ distanceValue.textContent="...";
+ if(distanceNote) distanceNote.textContent="Calculating driving distance...";
+ updatePrice({loading:true});
+ return;
+ }
+ distanceValue.textContent="--";
+ if(distanceNote) distanceNote.textContent="";
+ updatePrice();
+}
+
+function updateSwapState(){
+ const ready=Boolean(selectedPlaces.from && selectedPlaces.to);
+ swapBtn.disabled=!ready;
+}
+
+function updatePrice(opts={}){
+ if(opts.error){ price.textContent="$0"; priceNote.textContent=opts.error; return; }
+ if(opts.loading){ price.textContent="..."; priceNote.textContent="Waiting for distance to estimate fare."; return; }
+ if(typeof opts.miles==="number"){ const fare=calcFare(opts.miles); price.textContent=`$${fare}`; priceNote.textContent=`${distanceValue.textContent} miles driving`; return; }
+ price.textContent="$0"; priceNote.textContent="-- miles driving";
+}
+
+async function fetchMapboxSuggestions(q){
+ if(!MAPBOX_ACCESS_TOKEN) return;
+ const url=new URL("https://api.mapbox.com/search/searchbox/v1/suggest");
+ url.searchParams.set("q",q);
+ url.searchParams.set("language","en");
+ url.searchParams.set("limit",String(SUGGEST_LIMIT));
+ url.searchParams.set("types","poi,address,place");
+ url.searchParams.set("access_token",MAPBOX_ACCESS_TOKEN);
+ url.searchParams.set("session_token",MAPBOX_SESSION_TOKEN);
+ url.searchParams.set("country",MAPBOX_COUNTRY);
+ url.searchParams.set("bbox",MAPBOX_BBOX);
+ if(mapboxAbortController) mapboxAbortController.abort();
+ mapboxAbortController=new AbortController();
+ const res=await fetch(url.toString(),{signal:mapboxAbortController.signal});
+ if(!res.ok) throw new Error("Mapbox suggest failed");
+ return res.json();
+}
+async function fetchPlaceDetails(mapboxId){
+ if(!mapboxId) return null;
+ const url=new URL(`https://api.mapbox.com/search/searchbox/v1/retrieve/${encodeURIComponent(mapboxId)}`);
+ url.searchParams.set("session_token",MAPBOX_SESSION_TOKEN);
+ url.searchParams.set("access_token",MAPBOX_ACCESS_TOKEN);
+ const res=await fetch(url.toString());
+ if(!res.ok) throw new Error("Mapbox retrieve failed");
+ const data=await res.json();
+ const feature=data?.features?.[0];
+ const coords=feature?.geometry?.coordinates;
+ if(!coords || coords.length<2) return null;
+ return {coords:{lon:coords[0],lat:coords[1]}};
+}
+
+function metersToMiles(m){ return m/1609.344; }
+async function fetchDrivingDistance(fromCoords,toCoords,{signal}={}){
+ if(!fromCoords || !toCoords) throw new Error("Missing coords");
+ const coordsStr=`${fromCoords.lon},${fromCoords.lat};${toCoords.lon},${toCoords.lat}`;
+ const url=new URL(`https://api.mapbox.com/directions/v5/mapbox/driving/${coordsStr}`);
+ url.searchParams.set("access_token",MAPBOX_ACCESS_TOKEN);
+ url.searchParams.set("geometries","geojson");
+ url.searchParams.set("overview","false");
+ const res=await fetch(url.toString(),{signal});
+ if(!res.ok) throw new Error("Directions failed");
+ const data=await res.json();
+ const distanceMeters=data?.routes?.[0]?.distance;
+ if(typeof distanceMeters!=="number") throw new Error("No distance");
+ return metersToMiles(distanceMeters);
+}
+
+function maybeComputeDistance(){
+ const from=selectedPlaces.from;
+ const to=selectedPlaces.to;
+ if(!from || !to){ updateDistanceUI(); return; }
+ updateDistanceUI({loading:true});
+ if(distanceAbortController) distanceAbortController.abort();
+ distanceAbortController=new AbortController();
+ fetchDrivingDistance(from.coords,to.coords,{signal:distanceAbortController.signal})
+ .then(miles=>updateDistanceUI({miles}))
+ .catch(err=>{ if(err.name!=="AbortError") updateDistanceUI({error:"Could not fetch driving distance. Try again."}); });
+}
+
+function renderResults(field,suggestions){
+ if(!suggestions || !suggestions.length){ clearResults(field); return; }
+ const ul=document.createElement("ul");
+ suggestions.forEach(item=>{
+ const btn=document.createElement("button");
+ const headline=item.name || item.place_formatted || item.full_address || item.context?.place?.name || "Unknown";
+ const contextParts=[item.context?.address?.name,item.context?.street?.name,item.context?.place?.name,item.context?.region?.name,item.context?.postcode?.name,item.context?.country?.name].filter(Boolean);
+ const rawSelection=item.full_address || item.place_formatted || (headline && contextParts.length ? `${headline}, ${contextParts.join(", ")}` : (headline || contextParts.join(", ")));
+ const selectionValue=normalizeLocationText(rawSelection);
+ const label=formatSuggestionLabel(item, selectionValue, headline);
+ btn.innerHTML=`
📍${label}`;
+ btn.type="button";
+ btn.addEventListener("click",()=>{
+ const val=selectionValue || headline;
+ const displayLabel=abbreviateLocation(val);
+ showChip(field,displayLabel);
+ clearResults(field);
+ field.input.focus();
+ fetchPlaceDetails(item.mapbox_id)
+ .then(details=>{
+ if(details?.coords){ setSelectedPlace(field.key,{label:displayLabel,mapboxId:item.mapbox_id,coords:details.coords}); }
+ else { hideChip(field); updateDistanceUI({error:"Could not retrieve location details."}); }
+ })
+ .catch(err=>{ if(err.name!=="AbortError"){ hideChip(field); updateDistanceUI({error:"Could not retrieve location details."}); }});
+ });
+ const li=document.createElement("li");
+ li.appendChild(btn);
+ ul.appendChild(li);
+ });
+ field.results.innerHTML="";
+ field.results.appendChild(ul);
+}
+
+function clearResults(field){ field.results.innerHTML=""; }
+
+function handleInput(field){
+ const q=field.input.value.trim();
+ if(!q){ clearResults(field); return; }
+ hideChip(field);
+ fetchMapboxSuggestions(q)
+ .then(data=>renderResults(field,data?.suggestions||[]))
+ .catch(()=>{});
+}
+
+Object.values(mapboxFields).forEach(field=>{
+ field.input.addEventListener("input",()=>handleInput(field));
+ field.chipClear.addEventListener("click",()=>{
+ field.input.value="";
+ hideChip(field);
+ clearResults(field);
+ field.input.placeholder="Start typing a location";
+ field.input.focus();
+ });
+});
+
+document.addEventListener("click",e=>{
+ const fields=[fromLocation,toLocation,fromResults,toResults,fromChip,toChip];
+ if(fields.some(el=>el && el.contains(e.target))) return;
+ [fromResults,toResults].forEach(el=>el.innerHTML="");
+});
+
+swapBtn.addEventListener("click",()=>{
+ if(swapBtn.disabled) return;
+ const temp=selectedPlaces.from;
+ selectedPlaces.from=selectedPlaces.to;
+ selectedPlaces.to=temp;
+ if(selectedPlaces.from){ showChip(mapboxFields.from, selectedPlaces.from.label); }
+ if(selectedPlaces.to){ showChip(mapboxFields.to, selectedPlaces.to.label); }
+ updateSwapState();
+ maybeComputeDistance();
+});
+
+// Schedule default
+scheduleInput.step="300";
+scheduleInput.value=toLocalInput(roundToFiveMinutes(addHours(new Date(),1)));
+