From fb13f2c3ebe44865bd7b3b6b3caa2c61c9a77087 Mon Sep 17 00:00:00 2001 From: Shayan Date: Sun, 6 Apr 2025 16:14:34 +0500 Subject: [PATCH 1/6] issue#42-Making Find Distance card more engaging --- public/index.html | 55 ++++++++++++++++++++++++++--------------------- public/styles.css | 36 +++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/public/index.html b/public/index.html index 513c203..56d87bb 100644 --- a/public/index.html +++ b/public/index.html @@ -88,40 +88,47 @@

coordinates:

- -

Find Distance

- + +

+ Find Distance +

+ +

Measure the road distance and time between two locations.

+
- -
- + +
+ +
- - +
- -
- + +
+ +
- - - + +
+ + +
+
- Distance :
- Time : + Distance:
+ Estimated Time:
-


Directions courtesy of Valhalla (FOSSGIS)

+ +

Directions courtesy of Valhalla (FOSSGIS)

+
diff --git a/public/styles.css b/public/styles.css index 73ce670..07db23a 100644 --- a/public/styles.css +++ b/public/styles.css @@ -97,7 +97,6 @@ header button{ position: relative; width: 100%; min-width: 200px; - height: 30px; } #c-search-input input { @@ -461,4 +460,37 @@ footer { align-items: right; float: right; margin-left: 10px; -} \ No newline at end of file +} + +.box-input span i { + font-size: 18px; + margin-right: 10px; + color: #6F0E0E; +} + +.dist-input { + display: flex; + align-items: center; + flex: 1; + background-color: white; + border: 2px solid black; + border-radius: 10px; + padding: 5px 10px; +} + +.dist-input input { + flex: 1; + border: none; + outline: none; + font-size: 14px; + background-color: transparent; +} + +.dist-input button { + background: none; + border: none; + cursor: pointer; + font-size: 16px; + color: #777; + margin-left: 5px; +} From 86eefc0c55e4f64e10de15536ff1860917ed5090 Mon Sep 17 00:00:00 2001 From: Shayan Date: Sun, 6 Apr 2025 16:18:58 +0500 Subject: [PATCH 2/6] issue#42-Making Find Distance card more engaging --- public/styles.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/styles.css b/public/styles.css index 07db23a..a6627df 100644 --- a/public/styles.css +++ b/public/styles.css @@ -97,6 +97,7 @@ header button{ position: relative; width: 100%; min-width: 200px; + height: 30px; } #c-search-input input { @@ -462,6 +463,14 @@ footer { margin-left: 10px; } +.box .box-input { + display: flex; + align-items: center; + margin-bottom: 20px; + padding-left: 5px; + padding-right: 10px; +} + .box-input span i { font-size: 18px; margin-right: 10px; From 2bf3306786bee644fa98465073c94ee63ed48998 Mon Sep 17 00:00:00 2001 From: Shayan Date: Mon, 7 Apr 2025 12:14:28 +0500 Subject: [PATCH 3/6] issue#45-Add LocalStorage-based Search History for Place Search Input --- public/styles.css | 17 +++++- src/layout/add-listeners-to-ui.js | 3 + src/services/do-search.js | 91 +++++++++++++++++++++++++------ src/services/search-history.js | 53 ++++++++++++++++++ 4 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 src/services/search-history.js diff --git a/public/styles.css b/public/styles.css index a6627df..072cab5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -274,12 +274,23 @@ header button{ margin: 3.5px; background-color: #efeaea; border-radius: 3px; - padding: 2px; + padding: 8px; font-size: 13px; display: flex; - + align-items: center; + } + #search-results li:first-child { + background-color: transparent; + border-bottom: 1px solid #ddd; + margin-bottom: 8px; + padding: 8px; + } + #search-results li .fa-history { + color: #666; + margin-right: 8px; + font-size: 14px; } - #search-results li:hover{ + #search-results li:hover:not(:first-child) { background-color: rgb(223, 242, 236); } diff --git a/src/layout/add-listeners-to-ui.js b/src/layout/add-listeners-to-ui.js index 904b6c9..c524fc7 100644 --- a/src/layout/add-listeners-to-ui.js +++ b/src/layout/add-listeners-to-ui.js @@ -12,6 +12,7 @@ import { fetchCurrentLocation } from '../services/current-location.js'; import { closeSearchResultsOnClickOutside } from '../utils/general-events.js'; import { closeSound } from '../utils/sounds.js'; import { map } from '../components/map.js'; +import { initializeSearchInput } from '../services/do-search.js'; @@ -34,6 +35,8 @@ let minusIcon = document export function addListenerstoUI(){ + // Initialize search input event listeners + initializeSearchInput(); layersBtn.addEventListener('click', function () { layersDropdown.style.display = diff --git a/src/services/do-search.js b/src/services/do-search.js index 61cdacf..0c0ff56 100644 --- a/src/services/do-search.js +++ b/src/services/do-search.js @@ -7,11 +7,31 @@ import { notifyLoading, notifySreenReader } from "../utils/accessibility.js"; import { keyboardselect } from "../utils/keydown-helpers.js"; import { successSound } from "../utils/sounds.js"; import { geocodingAPI, headerofNominatim } from "../utils/to-km-or-meter.js"; +import { getSearchHistory, addToSearchHistory, createHistoryListItem } from "./search-history.js"; var placeIds = []; let searchLoadingInterval; // To store the interval globally for cancellation +// Initialize search input event listeners +export function initializeSearchInput() { + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.addEventListener('focus', () => { + const query = searchInput.value.trim(); + if (query.length <= 2) { + showSearchHistory(initializeResultsContainer(searchInput), searchInput); + } + }); + + searchInput.addEventListener('input', () => { + const query = searchInput.value.trim(); + if (query.length <= 2) { + showSearchHistory(initializeResultsContainer(searchInput), searchInput); + } + }); + } +} export function performSearch(inputField, excludedPlaceIds = []) { removeResults(); // Clear the previous search results @@ -20,15 +40,15 @@ export function performSearch(inputField, excludedPlaceIds = []) { const query = inputField.value.trim(); const loadingMessage = `
  • `; - // Clear results if query length is insufficient + // Initialize the search results container + let resultsContainer = initializeResultsContainer(inputField); + + // If query is empty or too short, show search history if (query.length <= 2) { - removeResults(); + showSearchHistory(resultsContainer, inputField, resolve); return; } - // Initialize the search results container - let resultsContainer = initializeResultsContainer(inputField); - // Display loading indicator resultsContainer.innerHTML = loadingMessage; searchLoadingInterval && clearInterval(searchLoadingInterval); @@ -40,6 +60,9 @@ export function performSearch(inputField, excludedPlaceIds = []) { .then((data) => { clearInterval(searchLoadingInterval); renderSearchResults(data, resultsContainer, inputField, resolve); + if (data.length > 0) { + addToSearchHistory(query); // Add to history if results were found + } }) .catch((error) => { clearInterval(searchLoadingInterval); @@ -49,6 +72,36 @@ export function performSearch(inputField, excludedPlaceIds = []) { }); } +// Show search history in the results container +function showSearchHistory(container, inputField, resolve) { + const history = getSearchHistory(); + container.innerHTML = ""; + + if (history.length === 0) { + addNoResultsMessage(container); + return; + } + + // Add a header for search history + const headerItem = document.createElement("li"); + headerItem.innerHTML = `Recent Searches`; + headerItem.style.backgroundColor = "transparent"; + headerItem.style.cursor = "default"; + container.appendChild(headerItem); + + history.forEach(query => { + const historyItem = createHistoryListItem(query, (selectedQuery) => { + inputField.value = selectedQuery; + if (resolve) { + performSearch(inputField, []).then(resolve); + } else { + performSearch(inputField, []); + } + }); + container.appendChild(historyItem); + }); +} + // Clears the search results and associated event listeners function clearSearchResults(searchResults) { if (searchResults) { @@ -59,17 +112,23 @@ function clearSearchResults(searchResults) { // Initializes the search results container function initializeResultsContainer(inputField) { - let container = inputField.nextElementSibling; - if (!container || container.tagName !== "UL") { - container = document.createElement("ul"); - container.id = "search-results"; - container.tabIndex = 7; - container.setAttribute("aria-label", "Select your result"); - inputField.parentElement.parentNode.insertBefore( - container, - inputField.parentNode.nextSibling - ); - } + // First, remove any existing search results containers + const existingContainers = document.querySelectorAll('#search-results'); + existingContainers.forEach(container => { + container.parentElement?.removeEventListener("keydown", keyboardselect); + container.remove(); + }); + + // Create new container + const container = document.createElement("ul"); + container.id = "search-results"; + container.tabIndex = 7; + container.setAttribute("aria-label", "Select your result"); + inputField.parentElement.parentNode.insertBefore( + container, + inputField.parentNode.nextSibling + ); + container.parentElement.addEventListener("keydown", keyboardselect); return container; } diff --git a/src/services/search-history.js b/src/services/search-history.js new file mode 100644 index 0000000..fb37ec2 --- /dev/null +++ b/src/services/search-history.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023-25 Zendalona + * This software is licensed under the GPL-3.0 License. + * See the LICENSE file in the root directory for more information. + */ + +const HISTORY_KEY = 'search_history'; +const MAX_HISTORY_ITEMS = 5; + +// Get search history from localStorage +export function getSearchHistory() { + const history = localStorage.getItem(HISTORY_KEY); + return history ? JSON.parse(history) : []; +} + +// Add a search query to history +export function addToSearchHistory(query) { + if (!query || query.trim().length <= 2) return; + + const history = getSearchHistory(); + const trimmedQuery = query.trim(); + + // Remove the query if it already exists (to avoid duplicates) + const filteredHistory = history.filter(item => item.toLowerCase() !== trimmedQuery.toLowerCase()); + + // Add new query to the beginning + filteredHistory.unshift(trimmedQuery); + + // Keep only the most recent MAX_HISTORY_ITEMS + while (filteredHistory.length > MAX_HISTORY_ITEMS) { + filteredHistory.pop(); + } + + // Save to localStorage + localStorage.setItem(HISTORY_KEY, JSON.stringify(filteredHistory)); +} + +// Clear search history +export function clearSearchHistory() { + localStorage.removeItem(HISTORY_KEY); +} + +// Create a history list item element +export function createHistoryListItem(query, onClick) { + const listItem = document.createElement('li'); + listItem.innerHTML = ` +   + ${query}`; + listItem.setAttribute('aria-label', `Recent search: ${query}`); + listItem.tabIndex = 1; + listItem.addEventListener('click', () => onClick(query)); + return listItem; +} \ No newline at end of file From 7ec1f6d9aa27abf81134501f7f1fbb6a57a167e1 Mon Sep 17 00:00:00 2001 From: Shayan Date: Mon, 7 Apr 2025 12:49:12 +0500 Subject: [PATCH 4/6] issue-47-bookmark-favorite-locations --- public/styles.css | 114 ++++++++++++++++++++++++++++++++++++++ src/services/bookmarks.js | 73 ++++++++++++++++++++++++ src/services/do-search.js | 82 +++++++++++++++++++++++++-- 3 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 src/services/bookmarks.js diff --git a/public/styles.css b/public/styles.css index 072cab5..340c325 100644 --- a/public/styles.css +++ b/public/styles.css @@ -514,3 +514,117 @@ footer { color: #777; margin-left: 5px; } + +#bookmarks-btn { + display: flex; + align-items: center; + gap: 5px; + margin: 5px 0; + padding: 8px 12px; + background-color: #fff; + border: 2px solid #000; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + color: #333; + transition: all 0.2s ease; +} + +#bookmarks-btn:hover { + background-color: #f0f0f0; +} + +#bookmarks-btn:focus { + outline: 2px solid #6F0E0E; + outline-offset: 2px; +} + +.bookmark-btn { + background: none; + border: none; + padding: 5px; + cursor: pointer; + color: #666; + margin-right: 8px; +} + +.bookmark-btn:hover { + color: #f1c40f; +} + +.bookmark-btn.active { + color: #f1c40f; +} + +.bookmark-item { + display: flex !important; + justify-content: space-between; + align-items: center; + padding: 8px !important; +} + +.bookmark-name { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bookmark-name i { + color: #6F0E0E; +} + +.bookmark-actions { + display: flex; + gap: 8px; +} + +.bookmark-actions button { + background: none; + border: none; + padding: 5px; + cursor: pointer; + color: #666; + transition: color 0.2s ease; +} + +.bookmark-actions .navigate-btn:hover { + color: #27ae60; +} + +.bookmark-actions .delete-btn:hover { + color: #e74c3c; +} + +.result-content { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#search-results li { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + margin: 3.5px; + background-color: #efeaea; + border-radius: 3px; + font-size: 13px; + cursor: pointer; +} + +#search-results li:first-child { + background-color: transparent; + border-bottom: 1px solid #ddd; + margin-bottom: 8px; + padding: 8px; +} + +#search-results li:hover:not(:first-child) { + background-color: rgb(223, 242, 236); +} diff --git a/src/services/bookmarks.js b/src/services/bookmarks.js new file mode 100644 index 0000000..2cb19ce --- /dev/null +++ b/src/services/bookmarks.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023-25 Zendalona + * This software is licensed under the GPL-3.0 License. + * See the LICENSE file in the root directory for more information. + */ + +import { map } from '../components/map.js'; +import { notifySreenReader } from '../utils/accessibility.js'; +import { successSound } from '../utils/sounds.js'; + +const BOOKMARKS_KEY = 'map_bookmarks'; + +// Get bookmarks from localStorage +export function getBookmarks() { + const bookmarks = localStorage.getItem(BOOKMARKS_KEY); + return bookmarks ? JSON.parse(bookmarks) : []; +} + +// Add a bookmark +export function addBookmark(name, lat, lng) { + const bookmarks = getBookmarks(); + // Check if bookmark already exists + if (!bookmarks.some(b => b.lat === lat && b.lng === lng)) { + bookmarks.unshift({ name, lat, lng }); + localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks)); + successSound.play(); + notifySreenReader('Location bookmarked'); + return true; + } + return false; +} + +// Remove a bookmark +export function removeBookmark(lat, lng) { + const bookmarks = getBookmarks(); + const filteredBookmarks = bookmarks.filter(b => b.lat !== lat || b.lng !== lng); + localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(filteredBookmarks)); + successSound.play(); + notifySreenReader('Bookmark removed'); +} + +// Navigate to a bookmarked location +export function navigateToBookmark(lat, lng) { + map.setView([lat, lng], 13); + successSound.play(); + notifySreenReader('Navigated to bookmarked location'); +} + +// Create a bookmark list item element +export function createBookmarkListItem(bookmark, onNavigate, onDelete) { + const listItem = document.createElement('li'); + listItem.className = 'bookmark-item'; + listItem.innerHTML = ` + + + ${bookmark.name} + +
    + + +
    + `; + + // Add event listeners + listItem.querySelector('.navigate-btn').addEventListener('click', onNavigate); + listItem.querySelector('.delete-btn').addEventListener('click', onDelete); + + return listItem; +} \ No newline at end of file diff --git a/src/services/do-search.js b/src/services/do-search.js index 0c0ff56..c51654c 100644 --- a/src/services/do-search.js +++ b/src/services/do-search.js @@ -8,6 +8,7 @@ import { keyboardselect } from "../utils/keydown-helpers.js"; import { successSound } from "../utils/sounds.js"; import { geocodingAPI, headerofNominatim } from "../utils/to-km-or-meter.js"; import { getSearchHistory, addToSearchHistory, createHistoryListItem } from "./search-history.js"; +import { getBookmarks, addBookmark, removeBookmark, navigateToBookmark, createBookmarkListItem } from "./bookmarks.js"; var placeIds = []; @@ -17,6 +18,20 @@ let searchLoadingInterval; // To store the interval globally for cancellation export function initializeSearchInput() { const searchInput = document.getElementById('search-input'); if (searchInput) { + // Add bookmarks button + const bookmarksBtn = document.createElement('button'); + bookmarksBtn.id = 'bookmarks-btn'; + bookmarksBtn.className = 'action-btn'; + bookmarksBtn.innerHTML = ' Bookmarks'; + bookmarksBtn.setAttribute('aria-label', 'Show bookmarks'); + searchInput.parentElement.parentNode.insertBefore(bookmarksBtn, searchInput.parentElement.nextSibling); + + // Add event listeners + bookmarksBtn.addEventListener('click', () => { + const container = initializeResultsContainer(searchInput); + showBookmarks(container); + }); + searchInput.addEventListener('focus', () => { const query = searchInput.value.trim(); if (query.length <= 2) { @@ -102,6 +117,37 @@ function showSearchHistory(container, inputField, resolve) { }); } +// Show bookmarks in the results container +function showBookmarks(container) { + const bookmarks = getBookmarks(); + container.innerHTML = ""; + + if (bookmarks.length === 0) { + addNoResultsMessage(container, "No bookmarks saved"); + return; + } + + // Add header + const headerItem = document.createElement("li"); + headerItem.innerHTML = `Saved Places`; + headerItem.style.backgroundColor = "transparent"; + headerItem.style.cursor = "default"; + container.appendChild(headerItem); + + // Add bookmarks + bookmarks.forEach(bookmark => { + const bookmarkItem = createBookmarkListItem( + bookmark, + () => navigateToBookmark(bookmark.lat, bookmark.lng), + () => { + removeBookmark(bookmark.lat, bookmark.lng); + showBookmarks(container); // Refresh the list + } + ); + container.appendChild(bookmarkItem); + }); +} + // Clears the search results and associated event listeners function clearSearchResults(searchResults) { if (searchResults) { @@ -169,9 +215,9 @@ function renderSearchResults(data, container, inputField, resolve) { } // Adds a "No Results Found" message -function addNoResultsMessage(container) { +function addNoResultsMessage(container, message = "No results found") { const noResultsItem = document.createElement("li"); - noResultsItem.textContent = "No results found"; + noResultsItem.textContent = message; container.appendChild(noResultsItem); } @@ -192,9 +238,37 @@ function addMoreResultsOption(container, inputField, placeIds, resolve) { // Creates a search result list item function createResultListItem(result, onClick) { const listItem = document.createElement("li"); + + // Create bookmark button + const bookmarkBtn = document.createElement("button"); + bookmarkBtn.className = "bookmark-btn"; + bookmarkBtn.innerHTML = ''; + bookmarkBtn.setAttribute("aria-label", `Bookmark ${result.display_name}`); + + // Add bookmark functionality + bookmarkBtn.addEventListener("click", (e) => { + e.stopPropagation(); // Prevent triggering the list item click + const success = addBookmark(result.display_name, parseFloat(result.lat), parseFloat(result.lon)); + if (success) { + bookmarkBtn.innerHTML = ''; + bookmarkBtn.classList.add("active"); + } + }); + + // Check if already bookmarked + const bookmarks = getBookmarks(); + if (bookmarks.some(b => b.lat === parseFloat(result.lat) && b.lng === parseFloat(result.lon))) { + bookmarkBtn.innerHTML = ''; + bookmarkBtn.classList.add("active"); + } + listItem.innerHTML = ` - ${result.type}  - ${result.display_name}`; +
    + ${result.type}  + ${result.display_name} +
    `; + + listItem.insertBefore(bookmarkBtn, listItem.firstChild); listItem.setAttribute("aria-label", result.display_name); listItem.tabIndex = 1; listItem.addEventListener("click", onClick); From 8cde9245cd4edca17c361f0d38f3d57401fd3f1c Mon Sep 17 00:00:00 2001 From: Shayan Date: Mon, 7 Apr 2025 21:28:47 +0500 Subject: [PATCH 5/6] issue#49-dynamic-routing --- public/index.html | 9 +- public/styles.css | 4 + server.js | 2 +- src/components/DistanceFinder/distance.js | 349 ++++++++++++++++------ src/components/map.js | 20 ++ src/services/bookmarks.js | 70 ++++- src/services/do-search.js | 11 +- 7 files changed, 369 insertions(+), 96 deletions(-) diff --git a/public/index.html b/public/index.html index 56d87bb..ee9d2ee 100644 --- a/public/index.html +++ b/public/index.html @@ -85,7 +85,7 @@

    coordinates:

    - +
    - +
    Distance:
    Estimated Time: +

    Directions courtesy of { - let filePath = '.' + req.url; + let filePath = '.' + req.url.split('?')[0]; // ✅ Strip query string if (filePath == './') filePath = './public/index.html'; // Default to index.html const extname = String(path.extname(filePath)).toLowerCase(); diff --git a/src/components/DistanceFinder/distance.js b/src/components/DistanceFinder/distance.js index 51fd2ec..e59ac71 100644 --- a/src/components/DistanceFinder/distance.js +++ b/src/components/DistanceFinder/distance.js @@ -1,13 +1,12 @@ - /* * Copyright (c) 2023-25 Zendalona * This software is licensed under the GPL-3.0 License. * See the LICENSE file in the root directory for more information. */ -import { geoLayer } from "../../services/fetch-place.js"; -import { performSearch } from "../../services/do-search.js"; -import { toKMorMeter } from "../../utils/to-km-or-meter.js"; +import { geoLayer, showPlaceDetails } from "../../services/fetch-place.js"; +import { performSearch, removeResults } from "../../services/do-search.js"; +import { geocodingAPI, headerofNominatim, toKMorMeter } from "../../utils/to-km-or-meter.js"; import { closeSound } from "../../utils/sounds.js"; import { FOSSGISValhallaEngine } from "./FOSSGISValhallaEngine.js"; @@ -16,6 +15,7 @@ import { successSound } from "../../utils/sounds.js"; import { adjustablePointer } from "../Marker/adjustable-pointer.js"; import Marker from "../Marker/marker.js"; import { notifySreenReader } from "../../utils/accessibility.js"; +import { map } from '../map.js'; // Tracks the currently focused input element (starting or destination location) let activeInputElement = null; @@ -25,6 +25,11 @@ const findDistanceButton = document.getElementById("find"); let startingLocationElement = document.getElementById("beginning"); // Element for user input of the destination location let destinationLocationElement = document.getElementById("destination"); +// Element to display the distance result +const distanceResultElement = document.getElementById("distanceResult"); +// Button to copy the route link +const copyRouteLinkButton = document.getElementById("copy-route-link-btn"); + // Coordinates for the destination let destinationCoordinates; // Coordinates for the starting location @@ -32,6 +37,144 @@ let startingCoordinates; // Layer group to represent the road path on the map let roadPathLayerGroup; +// --- URL Handling --- + +// Generate shareable route URL using place names +function generateRouteShareUrl(fromName, toName) { + if (!fromName || !toName) return null; + + const url = new URL(window.location.href); + url.searchParams.set('from', fromName); // no need to encode + url.searchParams.set('to', toName); + + // Remove unnecessary params + ['lat', 'lng', 'zoom', 'fromLat', 'fromLng', 'toLat', 'toLng'].forEach(param => + url.searchParams.delete(param) + ); + + return url.toString(); + } + + +// Update browser URL with route parameters using place names +function updateRouteUrl() { + const fromName = startingLocationElement.value; + const toName = destinationLocationElement.value; + const shareUrl = generateRouteShareUrl(fromName, toName); + if (shareUrl) { + window.history.pushState({ path: shareUrl }, '', shareUrl); + } +} + +// Geocode location name to get coordinates and display name +async function geocodeLocationByName(locationName) { + if (!locationName) return null; + try { + const url = `${geocodingAPI}/search.php?q=${encodeURIComponent(locationName)}&format=jsonv2&limit=1`; + const response = await fetch(url, headerofNominatim); + if (response.ok) { + const data = await response.json(); + if (data && data.length > 0) { + const bestResult = data[0]; + return { + lat: parseFloat(bestResult.lat), + lon: parseFloat(bestResult.lon), + name: bestResult.display_name // Use the display name from Nominatim + }; + } + } + } catch (error) { + console.error(`Geocoding failed for ${locationName}:`, error); + } + notifySreenReader(`Could not find coordinates for ${locationName}`); + return null; // Indicate failure +} + +// Perform reverse geocoding for a coordinate +async function reverseGeocode(lat, lon) { + try { + const response = await fetch( + `${geocodingAPI}/reverse?lat=${lat}&lon=${lon}&zoom=18&format=jsonv2`, + headerofNominatim + ); + if (response.ok) { + const data = await response.json(); + return data.display_name || `${lat.toFixed(3)}, ${lon.toFixed(3)}`; + } + } catch (error) { + console.error("Reverse geocoding failed:", error); + } + return `${lat.toFixed(3)}, ${lon.toFixed(3)}`; // Fallback name +} + +// Check URL parameters on load and initiate route calculation if present +async function checkRouteUrlParams() { + const urlParams = new URLSearchParams(window.location.search); + const fromName = urlParams.get('from'); + const toName = urlParams.get('to'); + + if (fromName && toName) { + const decodedFromName = decodeURIComponent(fromName); + const decodedToName = decodeURIComponent(toName); + + // Show the distance finder box + distanceBox.style.display = "block"; + startingLocationElement.value = decodedFromName; + destinationLocationElement.value = decodedToName; + notifySreenReader(`Loading route from ${decodedFromName} to ${decodedToName}`); + + // Geocode both locations + findDistanceButton.disabled = true; + findDistanceButton.innerHTML = ` Geocoding...`; + const startResult = await geocodeLocationByName(decodedFromName); + const endResult = await geocodeLocationByName(decodedToName); + findDistanceButton.innerHTML = ''; // Reset icon even if error + findDistanceButton.disabled = false; + + if (startResult && endResult) { + startingCoordinates = { lat: startResult.lat, lon: startResult.lon }; + destinationCoordinates = { lat: endResult.lat, lon: endResult.lon }; + // Update input fields with potentially more precise names from geocoding + startingLocationElement.value = startResult.name; + destinationLocationElement.value = endResult.name; + // Trigger distance calculation + calculateDistance(); + } else { + notifySreenReader("Could not find coordinates for one or both locations in the URL."); + // Optionally clear the fields or show an error message + // startingLocationElement.value = "Error loading location"; + // destinationLocationElement.value = "Error loading location"; + } + } +} + +// Copy route URL to clipboard using names +async function copyRouteUrlToClipboard() { + const fromName = startingLocationElement.value; + const toName = destinationLocationElement.value; + const shareUrl = generateRouteShareUrl(fromName, toName); + if (!shareUrl || !startingCoordinates || !destinationCoordinates) { // Also check if coords exist (route calculated) + notifySreenReader('Cannot copy link, route not calculated yet or locations invalid.'); + return; + } + try { + await navigator.clipboard.writeText(shareUrl); + notifySreenReader('Route link copied to clipboard'); + // Visual feedback + const originalText = copyRouteLinkButton.textContent; // Might not be needed if using innerHTML + copyRouteLinkButton.innerHTML = ' Copied!'; + setTimeout(() => { + // copyRouteLinkButton.textContent = originalText; + copyRouteLinkButton.innerHTML = ' Copy Route Link'; // Reset icon too + }, 2000); + } catch (err) { + console.error('Failed to copy route URL: ', err); + notifySreenReader('Failed to copy route link'); + } +} + +// --- End URL Handling --- + /** * Sets up event listeners for a search action triggered by an input element and a button. * @param {HTMLElement} inputElement - The input element for entering a location. @@ -55,7 +198,8 @@ export const initialize_DistanceFinder_EventListeners = () => { distanceBox.style.display = "block"; if(detalisElement.parentElement.style.display == 'block') detailsCloseButton.click(); // close search details box if (adjustablePointer) { - marker = new Marker(adjustablePointer.primaryMarker.getLatLng()).addTo(map); // Creates a new marker on the map + // Ensure marker is defined or initialized if needed + let marker = window.marker || new Marker(adjustablePointer.primaryMarker.getLatLng()).addTo(map); adjustablePointer.remove(); // Removes any active pointer on the map } successSound.play(); // Plays a sound to indicate action completion @@ -69,62 +213,92 @@ export const initialize_DistanceFinder_EventListeners = () => { [startingLocationElement, destinationLocationElement].forEach((inputElement) => { inputElement.addEventListener("focus", () => { activeInputElement = document.activeElement; - }); }); - // Handles selecting a location from the map - document.getElementById("fromMap")?.addEventListener("click", () => { - if (!marker) return; // Ensures a marker exists on the map + // Event listener for the main distance calculation button + findDistanceButton.addEventListener("click", calculateDistance); + + // Event listener for the copy route link button + copyRouteLinkButton?.addEventListener("click", copyRouteUrlToClipboard); - try { - const { lat, lng } = marker.getLatLng(); - activeInputElement.value = `${(lat.toFixed(5))},${lng.toFixed(5)}`; // Updates the active input with the selected coordinates - const selectedLocation = { lat:lat, lon: lng }; - - // Updates the appropriate variable based on the focused input - if (activeInputElement.id === "beginning") { - startingCoordinates = selectedLocation; - } else { - destinationCoordinates = selectedLocation; - } - - successSound.play(); // Plays a sound to confirm selection - } catch (error) { - console.error("Error selecting location from map:", error); - alert("Focus on the starting point or destination then select point on map"); - } - }); - - // Closes the distance finder box + // Close button for the distance finder box document.getElementById("closeBtn")?.addEventListener("click", closeDistanceFinder); - // Adds a keyboard shortcut to trigger the "select from map" action - distanceBox.addEventListener("keydown", (event) => { - if (event.altKey && event.key === "l") { - event.preventDefault(); - document.getElementById("fromMap")?.click(); // Simulates clicking the "fromMap" button - } - }); + // Choose from map button + document.getElementById("fromMap")?.addEventListener("click", chooseLocationFromMap); - // Triggers distance calculation when the "find" button is clicked - findDistanceButton.addEventListener("click", calculateDistance.bind(findDistanceButton)); + // Check for route parameters in URL on initial load + // We need to wait briefly for the map to potentially initialize from location params + // otherwise this might run before map.js sets the view based on lat/lng/zoom + setTimeout(checkRouteUrlParams, 500); }; +// Function to close the distance finder box and clear results +export function closeDistanceFinder() { + distanceBox.style.display = "none"; + distanceResultElement.style.display = "none"; + roadPathLayerGroup && roadPathLayerGroup.remove(); + startingLocationElement.value = ""; + destinationLocationElement.value = ""; + startingCoordinates = null; + destinationCoordinates = null; + copyRouteLinkButton.style.display = 'none'; // Hide copy button + // Clear route URL parameters + const url = new URL(window.location.href); + url.searchParams.delete('from'); + url.searchParams.delete('to'); + // Also clear old coord-based ones just in case + url.searchParams.delete('fromLat'); + url.searchParams.delete('fromLng'); + url.searchParams.delete('toLat'); + url.searchParams.delete('toLng'); + window.history.pushState({ path: url.toString() }, '', url.toString()); + closeSound.play(); +} + +// Function to allow user to choose a location from the map +function chooseLocationFromMap() { + if (!activeInputElement) { + notifySreenReader("Please focus on either the starting or destination input field first."); + return; + } + notifySreenReader("Map focused. Click on the map to select the location."); + map.getContainer().focus(); + map.once("click", (e) => { + const coords = { lat: e.latlng.lat, lon: e.latlng.lng }; + activeInputElement.value = "Loading..."; + reverseGeocode(coords.lat, coords.lon).then(name => { + activeInputElement.value = name; + if (activeInputElement.id === "beginning") { + startingCoordinates = coords; + } else if (activeInputElement.id === "destination") { + destinationCoordinates = coords; + } + successSound.play(); + notifySreenReader(`Selected ${name} for ${activeInputElement.id === 'beginning' ? 'starting point' : 'destination'}.`); + activeInputElement.focus(); // Return focus + }); + }); +} // Function to handle search and selection of the starting location export function handleStartingLocationSearch() { performSearch(startingLocationElement, []) .then((result) => { + if (!result) throw new Error("No result found"); // Handle case where performSearch resolves with no result startingCoordinates = { lat: parseFloat(result.lat), lon: parseFloat(result.lon), }; - startingLocationElement.value = result.name; - document.getElementById("search-results")?.remove(); + // Use display_name if available, otherwise use name + startingLocationElement.value = result.display_name || result.name; + removeResults(); }) .catch((error) => { - console.error("Error fetching search results:", error); + console.error("Error fetching start location search results:", error); + notifySreenReader("Could not find starting location."); + removeResults(); }); } @@ -132,33 +306,42 @@ export function handleStartingLocationSearch() { export function handleDestinationSearch() { performSearch(destinationLocationElement, []) .then((result) => { + if (!result) throw new Error("No result found"); // Handle case where performSearch resolves with no result destinationCoordinates = { lat: parseFloat(result.lat), lon: parseFloat(result.lon), }; - destinationLocationElement.value = result.name; - document.getElementById("search-results")?.remove(); + // Use display_name if available, otherwise use name + destinationLocationElement.value = result.display_name || result.name; + removeResults(); }) .catch((error) => { - console.error("Error fetching search results:", error); + console.error("Error fetching destination search results:", error); + notifySreenReader("Could not find destination location."); + removeResults(); }); } // Function to calculate and display the distance between the starting and destination locations export function calculateDistance() { - this.style.pointerEvents = 'none'; - this.innerHTML = ``; - this.className = ''; + if (!startingCoordinates || !destinationCoordinates) { + notifySreenReader("Please select both starting and destination points, or ensure locations from URL were found."); + return; + } + + findDistanceButton.disabled = true; + findDistanceButton.innerHTML = ` Finding...`; + distanceResultElement.style.display = "none"; + copyRouteLinkButton.style.display = 'none'; // Hide copy button initially const routePoints = [startingCoordinates, destinationCoordinates]; const route = FOSSGISValhallaEngine("route", "auto", routePoints); - route.getRoute(function (error, route) { - this.style.pointerEvents = 'auto'; - this.innerHTML = ''; - this.className = 'fas fa-arrow-circle-right'; + route.getRoute(function (error, routeResult) { // Renamed inner 'route' to 'routeResult' + findDistanceButton.disabled = false; + findDistanceButton.innerHTML = ''; // Reset icon - if (!error) { + if (!error && routeResult) { // Add the route line to the map if (geoLayer != null) { geoLayer.remove(); @@ -166,10 +349,11 @@ export function calculateDistance() { if (roadPathLayerGroup) { roadPathLayerGroup.remove(); } - marker.clearGeoJson(); + // Assuming window.marker is available globally or passed appropriately + window.marker?.clearGeoJson(); roadPathLayerGroup = L.featureGroup(); - const path = L.polyline(route.line, { color: "blue" }).addTo(roadPathLayerGroup); + const path = L.polyline(routeResult.line, { color: "blue" }).addTo(roadPathLayerGroup); L.circleMarker(path.getLatLngs()[0], { //adding starting point to map fillColor: "red", @@ -188,42 +372,31 @@ export function calculateDistance() { roadPathLayerGroup.addTo(map); map.fitBounds(roadPathLayerGroup.getBounds()); - document.getElementById("dist").innerHTML = toKMorMeter(route.distance*1000) - dist.text = `Distance: ${toKMorMeter(route.distance*1000)}`; - const timeElement = document.getElementById("time"); - if (route.time < 60) { - timeElement.innerHTML = `${route.time} Minutes`; - dist.text += `Time: ${route.time} Minutes`; - } else { - const hrs = parseInt(route.time / 60); - const min = route.time % 60; - timeElement.innerHTML = `${hrs} Hours ${min} Minutes`; - dist.text += `Time: ${hrs} Hours ${min} Minutes`; - } + const distanceText = toKMorMeter(routeResult.distance * 1000); + const timeInMinutes = parseInt(routeResult.time / 60); + const hours = Math.floor(timeInMinutes / 60); + const minutes = timeInMinutes % 60; + const timeText = `${hours > 0 ? hours + ' hr ' : ''}${minutes} min`; + + document.getElementById("dist").textContent = distanceText; + document.getElementById("time").textContent = timeText; + distanceResultElement.style.display = "block"; + copyRouteLinkButton.style.display = 'inline-block'; // Show copy button + notifySreenReader(`Route found. Distance: ${distanceText}. Estimated time: ${timeText}`); + updateRouteUrl(); // Update URL with names - notifySreenReader(dist.text); - document.getElementById("distanceResult").style.display = "block"; //showing the distance result } else { - const errorData = JSON.parse(route.responseText); - if (errorData.error_code === 130) { - alert("Failed to parse locations. Please ensure to select a valid location from suggestions."); - } else { - alert(errorData.error); + console.error("Error calculating route:", error); + // Provide more specific feedback if possible + let errorMsg = "Error calculating route."; + if (error && error.message) { + errorMsg = `Error calculating route: ${error.message}`; + } else if (typeof error === 'string') { + errorMsg = `Error calculating route: ${error}`; } + notifySreenReader(errorMsg); + distanceResultElement.style.display = "none"; + copyRouteLinkButton.style.display = 'none'; // Hide copy button on error } - }.bind(this)); -} - -// Function to close and reset the distance finder UI -export function closeDistanceFinder() { - if (roadPathLayerGroup) { - roadPathLayerGroup.remove(); - roadPathLayerGroup = null; - } - closeSound.play(); - document.getElementById("distanceResult").style.display = "none"; - startingLocationElement.value = ""; - destinationLocationElement.value = ""; - distanceBox.style.display = "none"; - notifySreenReader("Distance finder closed"); + }.bind(this)); } diff --git a/src/components/map.js b/src/components/map.js index 94ecd8d..58c8676 100644 --- a/src/components/map.js +++ b/src/components/map.js @@ -59,4 +59,24 @@ function calculateHeight() { L.control.scale().addTo(map); // this adds the visible scale to the map +// Initialize map view +map.setView([20, 0], 2); + +// Check for URL parameters on load +const urlParams = new URLSearchParams(window.location.search); +const lat = urlParams.get('lat'); +const lng = urlParams.get('lng'); +const zoom = urlParams.get('zoom'); +const fromName = urlParams.get('from'); // Check for the new name-based route param + +// Only set view from lat/lng/zoom if route params are NOT present +if (lat && lng && zoom && !fromName) { + const initialLat = parseFloat(lat); + const initialLng = parseFloat(lng); + const initialZoom = parseInt(zoom); + if (!isNaN(initialLat) && !isNaN(initialLng) && !isNaN(initialZoom)) { + map.setView([initialLat, initialLng], initialZoom); + } +} + diff --git a/src/services/bookmarks.js b/src/services/bookmarks.js index 2cb19ce..c819b1a 100644 --- a/src/services/bookmarks.js +++ b/src/services/bookmarks.js @@ -7,6 +7,8 @@ import { map } from '../components/map.js'; import { notifySreenReader } from '../utils/accessibility.js'; import { successSound } from '../utils/sounds.js'; +import { geocodingAPI, headerofNominatim } from "../utils/to-km-or-meter.js"; +import { showPlaceDetails } from './fetch-place.js'; const BOOKMARKS_KEY = 'map_bookmarks'; @@ -40,10 +42,63 @@ export function removeBookmark(lat, lng) { } // Navigate to a bookmarked location -export function navigateToBookmark(lat, lng) { - map.setView([lat, lng], 13); - successSound.play(); - notifySreenReader('Navigated to bookmarked location'); +export async function navigateToBookmark(lat, lng) { + const zoom = map.getZoom(); // Get current zoom level + map.setView([lat, lng], zoom); + updateUrl(lat, lng, zoom); // Update browser URL + + // Perform reverse geocoding to get place details + try { + const response = await fetch( + `${geocodingAPI}/reverse?lat=${lat}&lon=${lng}&zoom=18&format=jsonv2`, + headerofNominatim + ); + + if (response.ok) { + const data = await response.json(); + // Show place details + showPlaceDetails(data); + } + + successSound.play(); + notifySreenReader('Navigated to bookmarked location'); + } catch (error) { + console.error("Error fetching location details:", error); + notifySreenReader('Navigated to bookmarked location, but could not load details'); + } +} + +// Generate shareable URL +function generateShareUrl(lat, lng, zoom) { + const url = new URL(window.location.href); + url.searchParams.set('lat', lat.toFixed(5)); + url.searchParams.set('lng', lng.toFixed(5)); + url.searchParams.set('zoom', zoom); + return url.toString(); +} + +// Update browser URL without reloading +function updateUrl(lat, lng, zoom) { + const shareUrl = generateShareUrl(lat, lng, zoom); + window.history.pushState({ path: shareUrl }, '', shareUrl); +} + +// Copy URL to clipboard +async function copyUrlToClipboard(lat, lng, zoom, buttonElement) { + const shareUrl = generateShareUrl(lat, lng, zoom); + try { + await navigator.clipboard.writeText(shareUrl); + notifySreenReader('Link copied to clipboard'); + // Optional: Provide visual feedback on the button + const originalIcon = buttonElement.innerHTML; + buttonElement.innerHTML = ''; + setTimeout(() => { + buttonElement.innerHTML = originalIcon; + }, 1500); + } catch (err) { + console.error('Failed to copy URL: ', err); + notifySreenReader('Failed to copy link'); + } } // Create a bookmark list item element @@ -59,6 +114,9 @@ export function createBookmarkListItem(bookmark, onNavigate, onDelete) { + @@ -68,6 +126,10 @@ export function createBookmarkListItem(bookmark, onNavigate, onDelete) { // Add event listeners listItem.querySelector('.navigate-btn').addEventListener('click', onNavigate); listItem.querySelector('.delete-btn').addEventListener('click', onDelete); + listItem.querySelector('.share-btn').addEventListener('click', (e) => { + const zoom = map.getZoom(); + copyUrlToClipboard(bookmark.lat, bookmark.lng, zoom, e.currentTarget); + }); return listItem; } \ No newline at end of file diff --git a/src/services/do-search.js b/src/services/do-search.js index c51654c..1586914 100644 --- a/src/services/do-search.js +++ b/src/services/do-search.js @@ -138,7 +138,16 @@ function showBookmarks(container) { bookmarks.forEach(bookmark => { const bookmarkItem = createBookmarkListItem( bookmark, - () => navigateToBookmark(bookmark.lat, bookmark.lng), + async () => { + // Show loading indicator + container.innerHTML = `

  • `; + + // Navigate to the bookmark (this will also show place details) + await navigateToBookmark(bookmark.lat, bookmark.lng); + + // Close the results container + removeResults(); + }, () => { removeBookmark(bookmark.lat, bookmark.lng); showBookmarks(container); // Refresh the list From 7bceb0e6b261ca09e3af35156ba43df0da4c236c Mon Sep 17 00:00:00 2001 From: Shayan Date: Mon, 7 Apr 2025 21:28:47 +0500 Subject: [PATCH 6/6] issue#49-dynamic-routing --- public/index.html | 9 +- public/styles.css | 4 + server.js | 2 +- src/components/DistanceFinder/distance.js | 349 ++++++++++++++++------ src/components/map.js | 20 ++ src/services/bookmarks.js | 70 ++++- src/services/do-search.js | 11 +- 7 files changed, 369 insertions(+), 96 deletions(-) diff --git a/public/index.html b/public/index.html index 56d87bb..ee9d2ee 100644 --- a/public/index.html +++ b/public/index.html @@ -85,7 +85,7 @@

    coordinates:

    - +
    - +
    Distance:
    Estimated Time: +

    Directions courtesy of { - let filePath = '.' + req.url; + let filePath = '.' + req.url.split('?')[0]; // ✅ Strip query string if (filePath == './') filePath = './public/index.html'; // Default to index.html const extname = String(path.extname(filePath)).toLowerCase(); diff --git a/src/components/DistanceFinder/distance.js b/src/components/DistanceFinder/distance.js index 51fd2ec..e59ac71 100644 --- a/src/components/DistanceFinder/distance.js +++ b/src/components/DistanceFinder/distance.js @@ -1,13 +1,12 @@ - /* * Copyright (c) 2023-25 Zendalona * This software is licensed under the GPL-3.0 License. * See the LICENSE file in the root directory for more information. */ -import { geoLayer } from "../../services/fetch-place.js"; -import { performSearch } from "../../services/do-search.js"; -import { toKMorMeter } from "../../utils/to-km-or-meter.js"; +import { geoLayer, showPlaceDetails } from "../../services/fetch-place.js"; +import { performSearch, removeResults } from "../../services/do-search.js"; +import { geocodingAPI, headerofNominatim, toKMorMeter } from "../../utils/to-km-or-meter.js"; import { closeSound } from "../../utils/sounds.js"; import { FOSSGISValhallaEngine } from "./FOSSGISValhallaEngine.js"; @@ -16,6 +15,7 @@ import { successSound } from "../../utils/sounds.js"; import { adjustablePointer } from "../Marker/adjustable-pointer.js"; import Marker from "../Marker/marker.js"; import { notifySreenReader } from "../../utils/accessibility.js"; +import { map } from '../map.js'; // Tracks the currently focused input element (starting or destination location) let activeInputElement = null; @@ -25,6 +25,11 @@ const findDistanceButton = document.getElementById("find"); let startingLocationElement = document.getElementById("beginning"); // Element for user input of the destination location let destinationLocationElement = document.getElementById("destination"); +// Element to display the distance result +const distanceResultElement = document.getElementById("distanceResult"); +// Button to copy the route link +const copyRouteLinkButton = document.getElementById("copy-route-link-btn"); + // Coordinates for the destination let destinationCoordinates; // Coordinates for the starting location @@ -32,6 +37,144 @@ let startingCoordinates; // Layer group to represent the road path on the map let roadPathLayerGroup; +// --- URL Handling --- + +// Generate shareable route URL using place names +function generateRouteShareUrl(fromName, toName) { + if (!fromName || !toName) return null; + + const url = new URL(window.location.href); + url.searchParams.set('from', fromName); // no need to encode + url.searchParams.set('to', toName); + + // Remove unnecessary params + ['lat', 'lng', 'zoom', 'fromLat', 'fromLng', 'toLat', 'toLng'].forEach(param => + url.searchParams.delete(param) + ); + + return url.toString(); + } + + +// Update browser URL with route parameters using place names +function updateRouteUrl() { + const fromName = startingLocationElement.value; + const toName = destinationLocationElement.value; + const shareUrl = generateRouteShareUrl(fromName, toName); + if (shareUrl) { + window.history.pushState({ path: shareUrl }, '', shareUrl); + } +} + +// Geocode location name to get coordinates and display name +async function geocodeLocationByName(locationName) { + if (!locationName) return null; + try { + const url = `${geocodingAPI}/search.php?q=${encodeURIComponent(locationName)}&format=jsonv2&limit=1`; + const response = await fetch(url, headerofNominatim); + if (response.ok) { + const data = await response.json(); + if (data && data.length > 0) { + const bestResult = data[0]; + return { + lat: parseFloat(bestResult.lat), + lon: parseFloat(bestResult.lon), + name: bestResult.display_name // Use the display name from Nominatim + }; + } + } + } catch (error) { + console.error(`Geocoding failed for ${locationName}:`, error); + } + notifySreenReader(`Could not find coordinates for ${locationName}`); + return null; // Indicate failure +} + +// Perform reverse geocoding for a coordinate +async function reverseGeocode(lat, lon) { + try { + const response = await fetch( + `${geocodingAPI}/reverse?lat=${lat}&lon=${lon}&zoom=18&format=jsonv2`, + headerofNominatim + ); + if (response.ok) { + const data = await response.json(); + return data.display_name || `${lat.toFixed(3)}, ${lon.toFixed(3)}`; + } + } catch (error) { + console.error("Reverse geocoding failed:", error); + } + return `${lat.toFixed(3)}, ${lon.toFixed(3)}`; // Fallback name +} + +// Check URL parameters on load and initiate route calculation if present +async function checkRouteUrlParams() { + const urlParams = new URLSearchParams(window.location.search); + const fromName = urlParams.get('from'); + const toName = urlParams.get('to'); + + if (fromName && toName) { + const decodedFromName = decodeURIComponent(fromName); + const decodedToName = decodeURIComponent(toName); + + // Show the distance finder box + distanceBox.style.display = "block"; + startingLocationElement.value = decodedFromName; + destinationLocationElement.value = decodedToName; + notifySreenReader(`Loading route from ${decodedFromName} to ${decodedToName}`); + + // Geocode both locations + findDistanceButton.disabled = true; + findDistanceButton.innerHTML = ` Geocoding...`; + const startResult = await geocodeLocationByName(decodedFromName); + const endResult = await geocodeLocationByName(decodedToName); + findDistanceButton.innerHTML = ''; // Reset icon even if error + findDistanceButton.disabled = false; + + if (startResult && endResult) { + startingCoordinates = { lat: startResult.lat, lon: startResult.lon }; + destinationCoordinates = { lat: endResult.lat, lon: endResult.lon }; + // Update input fields with potentially more precise names from geocoding + startingLocationElement.value = startResult.name; + destinationLocationElement.value = endResult.name; + // Trigger distance calculation + calculateDistance(); + } else { + notifySreenReader("Could not find coordinates for one or both locations in the URL."); + // Optionally clear the fields or show an error message + // startingLocationElement.value = "Error loading location"; + // destinationLocationElement.value = "Error loading location"; + } + } +} + +// Copy route URL to clipboard using names +async function copyRouteUrlToClipboard() { + const fromName = startingLocationElement.value; + const toName = destinationLocationElement.value; + const shareUrl = generateRouteShareUrl(fromName, toName); + if (!shareUrl || !startingCoordinates || !destinationCoordinates) { // Also check if coords exist (route calculated) + notifySreenReader('Cannot copy link, route not calculated yet or locations invalid.'); + return; + } + try { + await navigator.clipboard.writeText(shareUrl); + notifySreenReader('Route link copied to clipboard'); + // Visual feedback + const originalText = copyRouteLinkButton.textContent; // Might not be needed if using innerHTML + copyRouteLinkButton.innerHTML = ' Copied!'; + setTimeout(() => { + // copyRouteLinkButton.textContent = originalText; + copyRouteLinkButton.innerHTML = ' Copy Route Link'; // Reset icon too + }, 2000); + } catch (err) { + console.error('Failed to copy route URL: ', err); + notifySreenReader('Failed to copy route link'); + } +} + +// --- End URL Handling --- + /** * Sets up event listeners for a search action triggered by an input element and a button. * @param {HTMLElement} inputElement - The input element for entering a location. @@ -55,7 +198,8 @@ export const initialize_DistanceFinder_EventListeners = () => { distanceBox.style.display = "block"; if(detalisElement.parentElement.style.display == 'block') detailsCloseButton.click(); // close search details box if (adjustablePointer) { - marker = new Marker(adjustablePointer.primaryMarker.getLatLng()).addTo(map); // Creates a new marker on the map + // Ensure marker is defined or initialized if needed + let marker = window.marker || new Marker(adjustablePointer.primaryMarker.getLatLng()).addTo(map); adjustablePointer.remove(); // Removes any active pointer on the map } successSound.play(); // Plays a sound to indicate action completion @@ -69,62 +213,92 @@ export const initialize_DistanceFinder_EventListeners = () => { [startingLocationElement, destinationLocationElement].forEach((inputElement) => { inputElement.addEventListener("focus", () => { activeInputElement = document.activeElement; - }); }); - // Handles selecting a location from the map - document.getElementById("fromMap")?.addEventListener("click", () => { - if (!marker) return; // Ensures a marker exists on the map + // Event listener for the main distance calculation button + findDistanceButton.addEventListener("click", calculateDistance); + + // Event listener for the copy route link button + copyRouteLinkButton?.addEventListener("click", copyRouteUrlToClipboard); - try { - const { lat, lng } = marker.getLatLng(); - activeInputElement.value = `${(lat.toFixed(5))},${lng.toFixed(5)}`; // Updates the active input with the selected coordinates - const selectedLocation = { lat:lat, lon: lng }; - - // Updates the appropriate variable based on the focused input - if (activeInputElement.id === "beginning") { - startingCoordinates = selectedLocation; - } else { - destinationCoordinates = selectedLocation; - } - - successSound.play(); // Plays a sound to confirm selection - } catch (error) { - console.error("Error selecting location from map:", error); - alert("Focus on the starting point or destination then select point on map"); - } - }); - - // Closes the distance finder box + // Close button for the distance finder box document.getElementById("closeBtn")?.addEventListener("click", closeDistanceFinder); - // Adds a keyboard shortcut to trigger the "select from map" action - distanceBox.addEventListener("keydown", (event) => { - if (event.altKey && event.key === "l") { - event.preventDefault(); - document.getElementById("fromMap")?.click(); // Simulates clicking the "fromMap" button - } - }); + // Choose from map button + document.getElementById("fromMap")?.addEventListener("click", chooseLocationFromMap); - // Triggers distance calculation when the "find" button is clicked - findDistanceButton.addEventListener("click", calculateDistance.bind(findDistanceButton)); + // Check for route parameters in URL on initial load + // We need to wait briefly for the map to potentially initialize from location params + // otherwise this might run before map.js sets the view based on lat/lng/zoom + setTimeout(checkRouteUrlParams, 500); }; +// Function to close the distance finder box and clear results +export function closeDistanceFinder() { + distanceBox.style.display = "none"; + distanceResultElement.style.display = "none"; + roadPathLayerGroup && roadPathLayerGroup.remove(); + startingLocationElement.value = ""; + destinationLocationElement.value = ""; + startingCoordinates = null; + destinationCoordinates = null; + copyRouteLinkButton.style.display = 'none'; // Hide copy button + // Clear route URL parameters + const url = new URL(window.location.href); + url.searchParams.delete('from'); + url.searchParams.delete('to'); + // Also clear old coord-based ones just in case + url.searchParams.delete('fromLat'); + url.searchParams.delete('fromLng'); + url.searchParams.delete('toLat'); + url.searchParams.delete('toLng'); + window.history.pushState({ path: url.toString() }, '', url.toString()); + closeSound.play(); +} + +// Function to allow user to choose a location from the map +function chooseLocationFromMap() { + if (!activeInputElement) { + notifySreenReader("Please focus on either the starting or destination input field first."); + return; + } + notifySreenReader("Map focused. Click on the map to select the location."); + map.getContainer().focus(); + map.once("click", (e) => { + const coords = { lat: e.latlng.lat, lon: e.latlng.lng }; + activeInputElement.value = "Loading..."; + reverseGeocode(coords.lat, coords.lon).then(name => { + activeInputElement.value = name; + if (activeInputElement.id === "beginning") { + startingCoordinates = coords; + } else if (activeInputElement.id === "destination") { + destinationCoordinates = coords; + } + successSound.play(); + notifySreenReader(`Selected ${name} for ${activeInputElement.id === 'beginning' ? 'starting point' : 'destination'}.`); + activeInputElement.focus(); // Return focus + }); + }); +} // Function to handle search and selection of the starting location export function handleStartingLocationSearch() { performSearch(startingLocationElement, []) .then((result) => { + if (!result) throw new Error("No result found"); // Handle case where performSearch resolves with no result startingCoordinates = { lat: parseFloat(result.lat), lon: parseFloat(result.lon), }; - startingLocationElement.value = result.name; - document.getElementById("search-results")?.remove(); + // Use display_name if available, otherwise use name + startingLocationElement.value = result.display_name || result.name; + removeResults(); }) .catch((error) => { - console.error("Error fetching search results:", error); + console.error("Error fetching start location search results:", error); + notifySreenReader("Could not find starting location."); + removeResults(); }); } @@ -132,33 +306,42 @@ export function handleStartingLocationSearch() { export function handleDestinationSearch() { performSearch(destinationLocationElement, []) .then((result) => { + if (!result) throw new Error("No result found"); // Handle case where performSearch resolves with no result destinationCoordinates = { lat: parseFloat(result.lat), lon: parseFloat(result.lon), }; - destinationLocationElement.value = result.name; - document.getElementById("search-results")?.remove(); + // Use display_name if available, otherwise use name + destinationLocationElement.value = result.display_name || result.name; + removeResults(); }) .catch((error) => { - console.error("Error fetching search results:", error); + console.error("Error fetching destination search results:", error); + notifySreenReader("Could not find destination location."); + removeResults(); }); } // Function to calculate and display the distance between the starting and destination locations export function calculateDistance() { - this.style.pointerEvents = 'none'; - this.innerHTML = ``; - this.className = ''; + if (!startingCoordinates || !destinationCoordinates) { + notifySreenReader("Please select both starting and destination points, or ensure locations from URL were found."); + return; + } + + findDistanceButton.disabled = true; + findDistanceButton.innerHTML = ` Finding...`; + distanceResultElement.style.display = "none"; + copyRouteLinkButton.style.display = 'none'; // Hide copy button initially const routePoints = [startingCoordinates, destinationCoordinates]; const route = FOSSGISValhallaEngine("route", "auto", routePoints); - route.getRoute(function (error, route) { - this.style.pointerEvents = 'auto'; - this.innerHTML = ''; - this.className = 'fas fa-arrow-circle-right'; + route.getRoute(function (error, routeResult) { // Renamed inner 'route' to 'routeResult' + findDistanceButton.disabled = false; + findDistanceButton.innerHTML = ''; // Reset icon - if (!error) { + if (!error && routeResult) { // Add the route line to the map if (geoLayer != null) { geoLayer.remove(); @@ -166,10 +349,11 @@ export function calculateDistance() { if (roadPathLayerGroup) { roadPathLayerGroup.remove(); } - marker.clearGeoJson(); + // Assuming window.marker is available globally or passed appropriately + window.marker?.clearGeoJson(); roadPathLayerGroup = L.featureGroup(); - const path = L.polyline(route.line, { color: "blue" }).addTo(roadPathLayerGroup); + const path = L.polyline(routeResult.line, { color: "blue" }).addTo(roadPathLayerGroup); L.circleMarker(path.getLatLngs()[0], { //adding starting point to map fillColor: "red", @@ -188,42 +372,31 @@ export function calculateDistance() { roadPathLayerGroup.addTo(map); map.fitBounds(roadPathLayerGroup.getBounds()); - document.getElementById("dist").innerHTML = toKMorMeter(route.distance*1000) - dist.text = `Distance: ${toKMorMeter(route.distance*1000)}`; - const timeElement = document.getElementById("time"); - if (route.time < 60) { - timeElement.innerHTML = `${route.time} Minutes`; - dist.text += `Time: ${route.time} Minutes`; - } else { - const hrs = parseInt(route.time / 60); - const min = route.time % 60; - timeElement.innerHTML = `${hrs} Hours ${min} Minutes`; - dist.text += `Time: ${hrs} Hours ${min} Minutes`; - } + const distanceText = toKMorMeter(routeResult.distance * 1000); + const timeInMinutes = parseInt(routeResult.time / 60); + const hours = Math.floor(timeInMinutes / 60); + const minutes = timeInMinutes % 60; + const timeText = `${hours > 0 ? hours + ' hr ' : ''}${minutes} min`; + + document.getElementById("dist").textContent = distanceText; + document.getElementById("time").textContent = timeText; + distanceResultElement.style.display = "block"; + copyRouteLinkButton.style.display = 'inline-block'; // Show copy button + notifySreenReader(`Route found. Distance: ${distanceText}. Estimated time: ${timeText}`); + updateRouteUrl(); // Update URL with names - notifySreenReader(dist.text); - document.getElementById("distanceResult").style.display = "block"; //showing the distance result } else { - const errorData = JSON.parse(route.responseText); - if (errorData.error_code === 130) { - alert("Failed to parse locations. Please ensure to select a valid location from suggestions."); - } else { - alert(errorData.error); + console.error("Error calculating route:", error); + // Provide more specific feedback if possible + let errorMsg = "Error calculating route."; + if (error && error.message) { + errorMsg = `Error calculating route: ${error.message}`; + } else if (typeof error === 'string') { + errorMsg = `Error calculating route: ${error}`; } + notifySreenReader(errorMsg); + distanceResultElement.style.display = "none"; + copyRouteLinkButton.style.display = 'none'; // Hide copy button on error } - }.bind(this)); -} - -// Function to close and reset the distance finder UI -export function closeDistanceFinder() { - if (roadPathLayerGroup) { - roadPathLayerGroup.remove(); - roadPathLayerGroup = null; - } - closeSound.play(); - document.getElementById("distanceResult").style.display = "none"; - startingLocationElement.value = ""; - destinationLocationElement.value = ""; - distanceBox.style.display = "none"; - notifySreenReader("Distance finder closed"); + }.bind(this)); } diff --git a/src/components/map.js b/src/components/map.js index 94ecd8d..58c8676 100644 --- a/src/components/map.js +++ b/src/components/map.js @@ -59,4 +59,24 @@ function calculateHeight() { L.control.scale().addTo(map); // this adds the visible scale to the map +// Initialize map view +map.setView([20, 0], 2); + +// Check for URL parameters on load +const urlParams = new URLSearchParams(window.location.search); +const lat = urlParams.get('lat'); +const lng = urlParams.get('lng'); +const zoom = urlParams.get('zoom'); +const fromName = urlParams.get('from'); // Check for the new name-based route param + +// Only set view from lat/lng/zoom if route params are NOT present +if (lat && lng && zoom && !fromName) { + const initialLat = parseFloat(lat); + const initialLng = parseFloat(lng); + const initialZoom = parseInt(zoom); + if (!isNaN(initialLat) && !isNaN(initialLng) && !isNaN(initialZoom)) { + map.setView([initialLat, initialLng], initialZoom); + } +} + diff --git a/src/services/bookmarks.js b/src/services/bookmarks.js index 2cb19ce..c819b1a 100644 --- a/src/services/bookmarks.js +++ b/src/services/bookmarks.js @@ -7,6 +7,8 @@ import { map } from '../components/map.js'; import { notifySreenReader } from '../utils/accessibility.js'; import { successSound } from '../utils/sounds.js'; +import { geocodingAPI, headerofNominatim } from "../utils/to-km-or-meter.js"; +import { showPlaceDetails } from './fetch-place.js'; const BOOKMARKS_KEY = 'map_bookmarks'; @@ -40,10 +42,63 @@ export function removeBookmark(lat, lng) { } // Navigate to a bookmarked location -export function navigateToBookmark(lat, lng) { - map.setView([lat, lng], 13); - successSound.play(); - notifySreenReader('Navigated to bookmarked location'); +export async function navigateToBookmark(lat, lng) { + const zoom = map.getZoom(); // Get current zoom level + map.setView([lat, lng], zoom); + updateUrl(lat, lng, zoom); // Update browser URL + + // Perform reverse geocoding to get place details + try { + const response = await fetch( + `${geocodingAPI}/reverse?lat=${lat}&lon=${lng}&zoom=18&format=jsonv2`, + headerofNominatim + ); + + if (response.ok) { + const data = await response.json(); + // Show place details + showPlaceDetails(data); + } + + successSound.play(); + notifySreenReader('Navigated to bookmarked location'); + } catch (error) { + console.error("Error fetching location details:", error); + notifySreenReader('Navigated to bookmarked location, but could not load details'); + } +} + +// Generate shareable URL +function generateShareUrl(lat, lng, zoom) { + const url = new URL(window.location.href); + url.searchParams.set('lat', lat.toFixed(5)); + url.searchParams.set('lng', lng.toFixed(5)); + url.searchParams.set('zoom', zoom); + return url.toString(); +} + +// Update browser URL without reloading +function updateUrl(lat, lng, zoom) { + const shareUrl = generateShareUrl(lat, lng, zoom); + window.history.pushState({ path: shareUrl }, '', shareUrl); +} + +// Copy URL to clipboard +async function copyUrlToClipboard(lat, lng, zoom, buttonElement) { + const shareUrl = generateShareUrl(lat, lng, zoom); + try { + await navigator.clipboard.writeText(shareUrl); + notifySreenReader('Link copied to clipboard'); + // Optional: Provide visual feedback on the button + const originalIcon = buttonElement.innerHTML; + buttonElement.innerHTML = ''; + setTimeout(() => { + buttonElement.innerHTML = originalIcon; + }, 1500); + } catch (err) { + console.error('Failed to copy URL: ', err); + notifySreenReader('Failed to copy link'); + } } // Create a bookmark list item element @@ -59,6 +114,9 @@ export function createBookmarkListItem(bookmark, onNavigate, onDelete) { + @@ -68,6 +126,10 @@ export function createBookmarkListItem(bookmark, onNavigate, onDelete) { // Add event listeners listItem.querySelector('.navigate-btn').addEventListener('click', onNavigate); listItem.querySelector('.delete-btn').addEventListener('click', onDelete); + listItem.querySelector('.share-btn').addEventListener('click', (e) => { + const zoom = map.getZoom(); + copyUrlToClipboard(bookmark.lat, bookmark.lng, zoom, e.currentTarget); + }); return listItem; } \ No newline at end of file diff --git a/src/services/do-search.js b/src/services/do-search.js index c51654c..1586914 100644 --- a/src/services/do-search.js +++ b/src/services/do-search.js @@ -138,7 +138,16 @@ function showBookmarks(container) { bookmarks.forEach(bookmark => { const bookmarkItem = createBookmarkListItem( bookmark, - () => navigateToBookmark(bookmark.lat, bookmark.lng), + async () => { + // Show loading indicator + container.innerHTML = `

  • `; + + // Navigate to the bookmark (this will also show place details) + await navigateToBookmark(bookmark.lat, bookmark.lng); + + // Close the results container + removeResults(); + }, () => { removeBookmark(bookmark.lat, bookmark.lng); showBookmarks(container); // Refresh the list