tweaks and fix of ray feature

This commit is contained in:
2026-01-25 15:52:46 +01:00
parent d97e26d881
commit 11bdb7009a
9 changed files with 147 additions and 349 deletions

View File

@@ -89,6 +89,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import maplibregl from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
@@ -111,13 +112,18 @@ import { KNOWN_SITES } from './data/historicSites.js';
// ============================================================================ // ============================================================================
// UTILITIES // UTILITIES
// ============================================================================ // ============================================================================
import { calculateDistance, calculateBearing, extendRay } from './utils/geometry.js'; import { calculateDistance, calculateBearing, extendRay, generateRayCoordinates } from './utils/geometry.js';
import { webMercatorToLonLat } from './utils/coordinates.js'; import { webMercatorToLonLat } from './utils/coordinates.js';
import { useTilesStore } from './stores/tiles.js'; import { useTilesStore } from './stores/tiles.js';
// For generating the pre-baked tiles: // For generating the pre-baked tiles:
// import { batchRenderTiles } from './utils/batch-renderer.js'; import { batchRenderTiles } from './utils/batch-renderer.js';
// batchRenderTiles(sandboxRef, tileCache, tileNames) { let tiles = [];
var batchRenderingActivated = false
if (tiles.length > 0) {
batchRenderingActivated = true
}
// ============================================================================ // ============================================================================
// CONSTANTS // CONSTANTS
@@ -258,11 +264,11 @@ async function loadTileImages(tileId, tileMetadata, imageUrl = null) {
const isOverwriting = imageUrl && tilesStore.areImagesOnMap(tileId); const isOverwriting = imageUrl && tilesStore.areImagesOnMap(tileId);
try { try {
// Determine which format to load (prefer PNG over JPG) // Determine which format to load
if (!imageUrl) { if (!imageUrl) {
imageUrl = tileMetadata.png_available imageUrl = tileMetadata.jpg_available
? tilesStore.getImageUrl(tileId, 'png') ? tilesStore.getImageUrl(tileId, 'jpg')
: tilesStore.getImageUrl(tileId, 'jpg'); : tilesStore.getImageUrl(tileId, 'png');
} }
// Remove old source/layer if overwriting // Remove old source/layer if overwriting
@@ -530,6 +536,14 @@ function onRenderComplete(result) {
function onRenderError(err) { function onRenderError(err) {
console.error('Renderer error:', err); console.error('Renderer error:', err);
} }
// ============================================================================
// BIBLIOGRAPHY
// ============================================================================
function showBibliography(citationKey) {
highlightedCitation.value = citationKey;
bibliographyVisible.value = true;
}
// ============================================================================ // ============================================================================
// GEOMETRY DRAWING // GEOMETRY DRAWING
@@ -568,13 +582,29 @@ function setDrawMode(mode) {
function completeDrawing() { function completeDrawing() {
const [pt1, pt2] = drawPoints.value; const [pt1, pt2] = drawPoints.value;
const coords = drawMode.value === 'ray'
? [pt1, extendRay(pt1[0], pt1[1], pt2[0], pt2[1], map.getBounds())] let coords;
: [pt1, pt2]; let properties = {
type: drawMode.value,
};
if (drawMode.value === 'ray') {
// Store the control points
properties.controlPoints = [pt1, pt2];
// Generate densely-sampled ray
coords = generateRayCoordinates(pt1[0], pt1[1], pt2[0], pt2[1], 1000);
} else {
// Regular line - just two points
coords = [pt1, pt2];
}
const distance = calculateDistance(coords[0][0], coords[0][1], coords[1][0], coords[1][1]); const distance = calculateDistance(coords[0][0], coords[0][1], coords[1][0], coords[1][1]);
const bearing = calculateBearing(coords[0][0], coords[0][1], coords[1][0], coords[1][1]); const bearing = calculateBearing(coords[0][0], coords[0][1], coords[1][0], coords[1][1]);
properties.length = distance;
properties.bearing = bearing;
const feature = { const feature = {
type: 'Feature', type: 'Feature',
id: `feature-${nextFeatureId.value++}`, id: `feature-${nextFeatureId.value++}`,
@@ -582,11 +612,7 @@ function completeDrawing() {
type: 'LineString', type: 'LineString',
coordinates: coords coordinates: coords
}, },
properties: { properties: properties
type: drawMode.value,
length: distance,
bearing: bearing
}
}; };
geometryFeatures.value.features.push(feature); geometryFeatures.value.features.push(feature);
@@ -642,15 +668,6 @@ function clearAllGeometry() {
} }
} }
// ============================================================================
// BIBLIOGRAPHY
// ============================================================================
function showBibliography(citationKey) {
highlightedCitation.value = citationKey;
bibliographyVisible.value = true;
}
// ============================================================================ // ============================================================================
// GEOMETRY LAYER UPDATES // GEOMETRY LAYER UPDATES
// ============================================================================ // ============================================================================
@@ -996,6 +1013,16 @@ onMounted(() => {
// Load initial tiles for known sites // Load initial tiles for known sites
await loadInitialTiles(); await loadInitialTiles();
if (batchRenderingActivated){
openSandbox();
let promises = tiles.flatMap(tile_id => [
tilesStore.fetchMoundData(tile_id, parseMoundBuffer),
tilesStore.fetchMetadataById(tile_id, parseMoundBuffer)
]);
await Promise.all(promises);
batchRenderTiles(sandboxRef.value, tilesStore, tiles)
}
}); });
}); });

View File

@@ -15,14 +15,21 @@
<button @click="$emit('delete', feature.id)" class="popup-btn danger">Delete</button> <button @click="$emit('delete', feature.id)" class="popup-btn danger">Delete</button>
</div> </div>
<!-- LINE or RAY --> <!-- LINE -->
<div v-else-if="type === 'line' || type === 'ray'"> <div v-else-if="type === 'line'">
<strong>{{ type === 'line' ? 'Line' : 'Ray' }}</strong> <strong>Line</strong>
<div>Length: {{ formatDistance(feature.properties.length, imperialUnits) }}</div> <div>Length: {{ formatDistance(feature.properties.length, imperialUnits) }}</div>
<div>Bearing: {{ formatBearing(feature.properties.bearing) }}</div> <div>Bearing: {{ formatBearing(feature.properties.bearing) }}</div>
<button @click="$emit('delete', feature.id)" class="popup-btn danger">Delete</button> <button @click="$emit('delete', feature.id)" class="popup-btn danger">Delete</button>
</div> </div>
<!-- RAY -->
<div v-else-if="type === 'line' || type === 'ray'">
<strong>Ray</strong>
<div>Bearing: {{ formatBearing(feature.properties.bearing) }}</div>
<button @click="$emit('delete', feature.id)" class="popup-btn danger">Delete</button>
</div>
<!-- HISTORIC SITE --> <!-- HISTORIC SITE -->
<div v-else-if="type === 'site'" class="site-popup"> <div v-else-if="type === 'site'" class="site-popup">
<strong>{{ feature.properties.name }}</strong> <strong>{{ feature.properties.name }}</strong>

View File

@@ -106,29 +106,11 @@
Imperial Units Imperial Units
</label> </label>
</div> </div>
<!-- ============================================= -->
<!-- SANDBOX BUTTON -->
<!-- ============================================= -->
<div class="control-section">
<button class="sandbox-btn" @click="$emit('openSandbox')">
Open Shading Sandbox
</button>
</div>
<!-- ============================================= -->
<!-- TILE REQUEST NOTIFICATIONS -->
<!-- ============================================= -->
<TileRequestNotification
:requests="tileRequests"
@dismiss="(id) => $emit('dismissRequest', id)"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { KNOWN_SITES } from '../data/historicSites.js'; import { KNOWN_SITES } from '../data/historicSites.js';
import TileRequestNotification from './TileRequestNotification.vue';
// ============================================================================ // ============================================================================
// INTERFACE // INTERFACE

View File

@@ -240,7 +240,6 @@ const initThreeJS = () => {
// Animation loop control // Animation loop control
const startAnimation = () => { const startAnimation = () => {
console.log('starting animation')
if (animationId) return; // Already running if (animationId) return; // Already running
const animate = () => { const animate = () => {
@@ -258,7 +257,6 @@ const startAnimation = () => {
}; };
const pauseAnimation = () => { const pauseAnimation = () => {
console.log('pausing animation')
animationPaused = true; animationPaused = true;
if (animationId) { if (animationId) {
cancelAnimationFrame(animationId); cancelAnimationFrame(animationId);
@@ -267,14 +265,12 @@ const pauseAnimation = () => {
}; };
const resumeAnimation = () => { const resumeAnimation = () => {
console.log('resuming animation')
animationPaused = false; animationPaused = false;
startAnimation(); startAnimation();
}; };
// Render a single frame (for when animation is paused but we need to update the view) // Render a single frame (for when animation is paused but we need to update the view)
const renderSingleFrame = () => { const renderSingleFrame = () => {
console.log('Rendering single frame')
if (renderer && scene && camera) { if (renderer && scene && camera) {
renderer.render(scene, camera); renderer.render(scene, camera);
} }
@@ -312,8 +308,7 @@ const handleResize = () => {
// Load tile data // Load tile data
const loadTileData = (tileData, newTileId) => { const loadTileData = (tileData, newTileId) => {
if (!scene) { if (!scene) {
console.error('Three.js not initialized'); initThreeJS();
return false;
} }
tileId.value = newTileId; tileId.value = newTileId;
@@ -547,8 +542,7 @@ const renderTile = async () => {
// Designed to be called repeatedly without setup/teardown overhead // Designed to be called repeatedly without setup/teardown overhead
const renderTileWithSettings = async (tileData, renderSettings, resolution = 1024) => { const renderTileWithSettings = async (tileData, renderSettings, resolution = 1024) => {
if (!renderer || !scene || !camera) { if (!renderer || !scene || !camera) {
console.error('Renderer not initialized'); initThreeJS();
return null;
} }
const startTime = performance.now(); const startTime = performance.now();

View File

@@ -1,224 +0,0 @@
<template>
<div v-if="activeRequests.length > 0" class="tile-notifications">
<!-- ============================================= -->
<!-- HEADER -->
<!-- ============================================= -->
<div class="notifications-header">
Tile Requests
</div>
<!-- ============================================= -->
<!-- ACTIVE REQUESTS -->
<!-- ============================================= -->
<div
v-for="request in activeRequests"
:key="request.id"
:class="['notification-item', `status-${request.status}`]"
>
<div class="notification-content">
<div class="notification-location">
{{ formatLocation(request.lat, request.lng) }}
</div>
<div class="notification-status">
<span class="status-icon">{{ getStatusIcon(request.status) }}</span>
<span class="status-text">{{ getStatusText(request.status) }}</span>
</div>
<div v-if="request.message" class="notification-message">
{{ request.message }}
</div>
</div>
<!-- Close button for completed/failed requests -->
<button
v-if="request.status === 'ready' || request.status === 'error'"
@click="$emit('dismiss', request.id)"
class="dismiss-btn"
title="Dismiss"
>
×
</button>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
// ============================================================================
// INTERFACE
// ============================================================================
const props = defineProps({
requests: {
type: Object, // { requestId: { lat, lng, status, message, tileId } }
required: true
}
});
defineEmits(['dismiss']);
// ============================================================================
// COMPUTED
// ============================================================================
const activeRequests = computed(() => {
return Object.entries(props.requests).map(([id, data]) => ({
id,
...data
}));
});
// ============================================================================
// METHODS
// ============================================================================
function formatLocation(lat, lng) {
return `${lat.toFixed(4)}, ${lng.toFixed(4)}`;
}
function getStatusIcon(status) {
switch (status) {
case 'looking_up': return '🔍';
case 'found': return '✓';
case 'processing': return '⚙️';
case 'ready': return '✅';
case 'error': return '❌';
default: return '•';
}
}
function getStatusText(status) {
switch (status) {
case 'looking_up': return 'Finding tile...';
case 'found': return 'Tile found';
case 'processing': return 'Processing...';
case 'ready': return 'Ready!';
case 'error': return 'Failed';
default: return status;
}
}
</script>
<style scoped>
/* ============================================= */
/* NOTIFICATIONS CONTAINER */
/* ============================================= */
.tile-notifications {
margin-top: 15px;
padding-top: 15px;
border-top: 2px solid #ddd;
}
/* ============================================= */
/* HEADER */
/* ============================================= */
.notifications-header {
font-weight: 600;
font-size: 13px;
color: #666;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ============================================= */
/* NOTIFICATION ITEMS */
/* ============================================= */
.notification-item {
background: #f9f9f9;
border-radius: 4px;
padding: 10px;
margin-bottom: 8px;
border-left: 3px solid #ccc;
position: relative;
transition: all 0.3s;
}
.notification-item.status-looking_up {
border-left-color: #4A9EFF;
background: #f0f7ff;
}
.notification-item.status-found {
border-left-color: #4CAF50;
background: #f1f8f4;
}
.notification-item.status-processing {
border-left-color: #FF9800;
background: #fff8f0;
}
.notification-item.status-ready {
border-left-color: #4CAF50;
background: #f1f8f4;
}
.notification-item.status-error {
border-left-color: #f44336;
background: #fff0f0;
}
/* ============================================= */
/* NOTIFICATION CONTENT */
/* ============================================= */
.notification-content {
padding-right: 24px; /* Space for dismiss button */
}
.notification-location {
font-family: monospace;
font-size: 11px;
color: #999;
margin-bottom: 4px;
}
.notification-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.status-icon {
font-size: 14px;
}
.notification-message {
font-size: 12px;
color: #666;
margin-top: 4px;
font-style: italic;
}
/* ============================================= */
/* DISMISS BUTTON */
/* ============================================= */
.dismiss-btn {
position: absolute;
top: 8px;
right: 8px;
width: 20px;
height: 20px;
background: none;
border: none;
font-size: 18px;
line-height: 1;
cursor: pointer;
color: #999;
transition: color 0.2s;
padding: 0;
}
.dismiss-btn:hover {
color: #333;
}
</style>

View File

@@ -46,15 +46,15 @@ export const KNOWN_SITES = [
"description": "A nearly perfect circle 1,200 feet in diameter, enclosing approximately 30 acres \\cite{squier_davis_1848}. The earthen wall is lined by a deep interior ditch, a design typical of earlier Adena earthworks \\cite{lynott_2015}. Located in Heath, Ohio, this is one of the largest circular earthworks in the Americas. Eagle Mound at the center covers the remains of a large ceremonial structure \\cite{ohc_newark}. The walls vary from 4 to 14 feet in height at the monumental gateway. Unlike the Octagon, no solar alignments have been confirmed at the Great Circle \\cite{hively_horn_1982}. Part of the Hopewell Ceremonial Earthworks UNESCO World Heritage Site \\cite{unesco_2023}.", "description": "A nearly perfect circle 1,200 feet in diameter, enclosing approximately 30 acres \\cite{squier_davis_1848}. The earthen wall is lined by a deep interior ditch, a design typical of earlier Adena earthworks \\cite{lynott_2015}. Located in Heath, Ohio, this is one of the largest circular earthworks in the Americas. Eagle Mound at the center covers the remains of a large ceremonial structure \\cite{ohc_newark}. The walls vary from 4 to 14 feet in height at the monumental gateway. Unlike the Octagon, no solar alignments have been confirmed at the Great Circle \\cite{hively_horn_1982}. Part of the Hopewell Ceremonial Earthworks UNESCO World Heritage Site \\cite{unesco_2023}.",
"type": "earthwork", "type": "earthwork",
"tiles": ["BS19870742", 'BS19870743', 'BS19880743'], "tiles": ["BS19870742", 'BS19870743', 'BS19880743'],
"overlay" : [ // "overlay" : [
{ // {
"type": 'line', // "type": 'line',
"coordinates": [ // "coordinates": [
[-82.459197, 40.027871], // [-82.459197, 40.027871],
[-82.458565, 40.028731] // [-82.458565, 40.028731]
] // ]
} // }
] // ]
}, },
{ {
"name": "Van Voorhis Walls", "name": "Van Voorhis Walls",
@@ -91,6 +91,13 @@ export const KNOWN_SITES = [
"type": "earthwork", "type": "earthwork",
"tiles": ['BS17630452', 'BS17630451', 'BS17650451', 'BS17620451', 'BS17620450'] "tiles": ['BS17630452', 'BS17630451', 'BS17650451', 'BS17620451', 'BS17620450']
}, },
{
"name": "Southernmost identfied Section",
"description":"Southernmost identfied GHR Section according to \\cite{lepper_2024}",
"coordinates": [[-82.52056,39.95528]],
"type": 'road_confirmed',
"tiles": ['BS19620711',"BS19610711"]
},
// There is something in this area, but I can't confirm it's the 'high banks works' // There is something in this area, but I can't confirm it's the 'high banks works'
// Google maps and some facebook boomer do claim so, "highbank park earthworks" // Google maps and some facebook boomer do claim so, "highbank park earthworks"
// A historical map puts it near the Scioto river, but that's on the other side of columbus // A historical map puts it near the Scioto river, but that's on the other side of columbus

View File

@@ -1,13 +1,20 @@
/** /**
* Batch Renderer for Hopewell Lidar Tiles * Batch Renderer for Hopewell Lidar Tiles
* *
* Usage in dev console with loaded app: * Usage:
* import { batchRenderTiles } from './batch-renderer.js'; * import { batchRenderTiles } from './batch-renderer.js';
* const app = document.querySelector('#app').__vue_app__;
* const sandboxRef = app._instance.refs.sandboxRef;
* const tileCache = app._instance.data.tileCache;
* *
* await batchRenderTiles(sandboxRef, tileCache, tileNames); * // Get refs from your app
* const tilesStore = useTilesStore();
* const sandboxRef = ref(null); // ref to ShadingSandbox component
*
* // Render tiles
* const results = await batchRenderTiles(
* sandboxRef.value,
* tilesStore,
* ['tile-id-1', 'tile-id-2', 'tile-id-3'],
* { renderQuality: 1024 }
* );
*/ */
/** /**
@@ -23,62 +30,53 @@ function downloadDataURL(dataURL, filename) {
} }
/** /**
* Download text content as a file * Batch render tiles using offscreen ShadingSandbox
*/
function downloadText(text, filename) {
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Batch render tiles using already-loaded app
* *
* @param {Object} sandboxRef - Vue ref to ShadingSandbox component * @param {Object} sandboxComponent - Vue component instance (sandboxRef.value)
* @param {Object} tileCache - Cache of loaded tile data * @param {Object} tilesStore - Pinia tiles store instance
* @param {string[]} tileNames - Array of tile names to render * @param {string[]} tileIds - Array of tile IDs to render (must be pre-loaded in store)
* @param {Object} options - Options * @param {Object} options - Options
* @param {Object} options.renderSettings - Override render settings * @param {number} options.renderQuality - Render quality in pixels (default: 1024)
* @param {number} options.renderQuality - Render quality (default: 1024)
* *
* @returns {Promise<Object[]>} Array of results * @returns {Promise<Object[]>} Array of results
*/ */
export async function batchRenderTiles(sandboxRef, tileCache, tileNames, options = {}) { export async function batchRenderTiles(sandboxComponent, tilesStore, tileIds, options = {}) {
const { const {
renderSettings = null, // Use sandbox defaults if null
renderQuality = 1024 renderQuality = 1024
} = options; } = options;
console.log(`[BatchRenderer] Starting batch render of ${tileNames.length} tiles`); console.log(`[BatchRenderer] Starting batch render of ${tileIds.length} tiles`);
console.log(`[BatchRenderer] Quality: ${renderQuality}px`); console.log(`[BatchRenderer] Quality: ${renderQuality}px`);
const results = []; const results = [];
const startTime = Date.now(); const startTime = Date.now();
sandboxComponent.offscreen = true;
for (let i = 0; i < tileNames.length; i++) { for (let i = 0; i < tileIds.length; i++) {
const tileName = tileNames[i]; const tileId = tileIds[i];
const current = i + 1; const current = i + 1;
const total = tileNames.length; const total = tileIds.length;
console.log(`[BatchRenderer] [${current}/${total}] Processing ${tileName}...`); console.log(`[BatchRenderer] [${current}/${total}] Processing ${tileId}...`);
try { try {
const tileData = tileCache[tileName]; // Get tile data from store
if (!tileData) { const metadata = tilesStore.getMetadata(tileId);
throw new Error(`Tile ${tileName} not found in cache`); const moundData = tilesStore.getMoundData(tileId);
if (!metadata) {
throw new Error(`Metadata not found in store for ${tileId}`);
} }
// Render if (!moundData) {
console.log(`[BatchRenderer] Rendering...`); throw new Error(`Mound data not found in store for ${tileId}`);
const renderResult = await sandboxRef.renderTileWithSettings( }
tileData,
renderSettings || sandboxRef.getSettings(), // Render with current settings
console.log(`[BatchRenderer] Rendering at ${renderQuality}px...`);
const renderResult = await sandboxComponent.renderTileWithSettings(
moundData,
sandboxComponent.getSettings(),
renderQuality renderQuality
); );
@@ -88,37 +86,21 @@ export async function batchRenderTiles(sandboxRef, tileCache, tileNames, options
console.log(`[BatchRenderer] Rendered in ${renderResult.renderTime}ms`); console.log(`[BatchRenderer] Rendered in ${renderResult.renderTime}ms`);
// Generate metadata // Download PNG
const metadata = { downloadDataURL(renderResult.dataURL, `${tileId}.png`);
tileName,
bounds: tileData.bounds,
renderSettings: renderSettings || sandboxRef.getSettings(),
renderQuality,
pointCount: tileData.pointCount,
triangleCount: tileData.triangleCount,
renderedAt: new Date().toISOString()
};
// Download
downloadDataURL(renderResult.dataURL, `${tileName}.png`);
downloadText(JSON.stringify(metadata, null, 2), `${tileName}.json`);
results.push({ results.push({
tileName, tileId,
metadata,
renderTime: renderResult.renderTime, renderTime: renderResult.renderTime,
success: true success: true
}); });
console.log(`[BatchRenderer] ✓ Complete`); console.log(`[BatchRenderer] ✓ Complete`);
// Small delay
await new Promise(resolve => setTimeout(resolve, 100));
} catch (err) { } catch (err) {
console.error(`[BatchRenderer] ✗ Failed: ${err.message}`); console.error(`[BatchRenderer] ✗ Failed: ${err.message}`);
results.push({ results.push({
tileName, tileId,
success: false, success: false,
error: err.message error: err.message
}); });

View File

@@ -38,7 +38,7 @@ export function formatDistance(meters, useImperial = false) {
* Format bearing angle * Format bearing angle
*/ */
export function formatBearing(degrees) { export function formatBearing(degrees) {
return `${degrees.toFixed(1)}°`; return `${degrees.toFixed(2)}°`;
} }
/** /**

View File

@@ -40,7 +40,7 @@ export function calculateBearing(lng1, lat1, lng2, lat2) {
* Extend a line from point1 through point2 to a far distance (100km) * Extend a line from point1 through point2 to a far distance (100km)
* Used for ray drawing * Used for ray drawing
*/ */
export function extendRay(lng1, lat1, lng2, lat2, bounds) { export function extendRay(lng1, lat1, lng2, lat2) {
const bearing = calculateBearing(lng1, lat1, lng2, lat2); const bearing = calculateBearing(lng1, lat1, lng2, lat2);
const bearingRad = bearing * Math.PI / 180; const bearingRad = bearing * Math.PI / 180;
@@ -57,3 +57,26 @@ export function extendRay(lng1, lat1, lng2, lat2, bounds) {
return [labmda_2 * 180 / Math.PI, phi_2 * 180 / Math.PI]; return [labmda_2 * 180 / Math.PI, phi_2 * 180 / Math.PI];
} }
/**
* Generate a densely-sampled ray from pt1 through pt2
* Creates many intermediate points so the line appears straight in Mercator projection
*/
export function generateRayCoordinates(lng1, lat1, lng2, lat2, numSegments = 1000) {
const dLng = lng2 - lng1;
const dLat = lat2 - lat1;
// Extend the ray far beyond point 2
const extensionFactor = 100;
const coords = [];
for (let i = 0; i <= numSegments; i++) {
const t = (i / numSegments) * extensionFactor;
coords.push([
lng1 + dLng * t,
lat1 + dLat * t
]);
}
return coords;
}