From d418e4f9b9187575ad41f1cfeb51d81425352612 Mon Sep 17 00:00:00 2001 From: Rub21 Date: Thu, 9 Jul 2026 17:01:20 -0500 Subject: [PATCH 1/4] Add start_date and end_date to Nominatim search results --- app/controllers/searches/nominatim_queries_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/searches/nominatim_queries_controller.rb b/app/controllers/searches/nominatim_queries_controller.rb index 43ec532d6b3..26d9ae6592c 100644 --- a/app/controllers/searches/nominatim_queries_controller.rb +++ b/app/controllers/searches/nominatim_queries_controller.rb @@ -74,6 +74,7 @@ def create :min_lat => min_lat, :max_lat => max_lat, :min_lon => min_lon, :max_lon => max_lon, :prefix => prefix, :name => name, :suffix => suffix, + :start_date => start_date, :end_date => end_date, :type => object_type, :id => object_id) end rescue StandardError => e From f8109f3d53665df3f9acc166b8f4241e78777a11 Mon Sep 17 00:00:00 2001 From: Rub21 Date: Thu, 9 Jul 2026 17:32:04 -0500 Subject: [PATCH 2/4] Fix time slider when selecting a search result --- app/assets/javascripts/index/search.js | 85 +++++++++++++++++++++----- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/index/search.js b/app/assets/javascripts/index/search.js index 986982cd545..656aaeea930 100644 --- a/app/assets/javascripts/index/search.js +++ b/app/assets/javascripts/index/search.js @@ -91,10 +91,56 @@ OSM.Search = function (map) { } } + // Compare two ISO 8601 dates, tolerating partial (year, year-month) and BCE values. + // Same logic as compareDates() in query.js; kept local until we share a date util. + function compareDates(date1, date2) { + const match1 = date1.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); + const match2 = date2.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); + if (!match1 || !match2) return date1.localeCompare(date2); + + const [, year1, month1, day1] = match1; + const [, year2, month2, day2] = match2; + if (parseInt(year1, 10) !== parseInt(year2, 10)) return parseInt(year1, 10) - parseInt(year2, 10); + + const m1 = month1 ? parseInt(month1, 10) : 1; + const m2 = month2 ? parseInt(month2, 10) : 1; + if (m1 !== m2) return m1 - m2; + + return (day1 ? parseInt(day1, 10) : 1) - (day2 ? parseInt(day2, 10) : 1); + } + + // The slider only accepts full YYYY-MM-DD, so pad a year or year-month before setDate(). + function padDate(value) { + if (value === null || value === undefined || value === "") return null; + const parts = String(value).trim().match(/^(-?\d{1,4})(?:-(\d{2}))?(?:-(\d{2}))?/); + if (!parts) return null; + return `${parts[1]}-${parts[2] || "01"}-${parts[3] || "01"}`; + } + + // Move the slider so the picked feature is visible. If it already exists at the + // current time we leave the slider alone. + function adjustTimeSliderToResult(data) { + if (!map.timeslider) return; + + const startDate = data.startDate != null && data.startDate !== "" ? String(data.startDate) : null; + const endDate = data.endDate != null && data.endDate !== "" ? String(data.endDate) : null; + if (!startDate && !endDate) return; + + const current = map.timeslider.getDate(); + const afterStart = !startDate || compareDates(startDate, current) <= 0; + const beforeEnd = !endDate || compareDates(endDate, current) > 0; + if (afterStart && beforeEnd) return; // already visible now + + // out of view: jump to start_date, or to end_date when there is no start + const target = padDate(startDate || endDate); + if (target) map.timeslider.setDate(target); + } + function clickSearchResult(e) { const data = $(this).data(); panToSearchResult(data); + adjustTimeSliderToResult(data); // Let clicks to object browser links propagate. if (data.type && data.id) return; @@ -116,23 +162,34 @@ OSM.Search = function (map) { }; page.load = function () { - $(".search_results_entry[data-href]").each(function (index) { - const entry = $(this); - fetchReplace(this.dataset, entry.children().first()) - .then(() => { - // go to first result of first geocoder - if (index === 0) { - const firstResult = entry.find("*[data-lat][data-lon]:first").first(); - if (firstResult.length) { - panToSearchResult(firstResult.data()); + // the original page.load content is the function below, and is used when one visits this page, be it first load OR later routing change + // below, we wrap "if map.timeslider" so we only try to add the timeslider if we don't already have it + function originalLoadFunction () { + $(".search_results_entry[data-href]").each(function (index) { + const entry = $(this); + fetchReplace(this.dataset, entry.children().first()) + .then(() => { + // go to first result of first geocoder + if (index === 0) { + const firstResult = entry.find("*[data-lat][data-lon]:first").first(); + if (firstResult.length) { + panToSearchResult(firstResult.data()); + } } - } - }); - }); + }); + }); - addOpenHistoricalMapTimeSlider(map); + return map.getState(); + } // end originalLoadFunction - return map.getState(); + // "if map.timeslider" only try to add the timeslider if we don't already have it + if (map.timeslider) { + originalLoadFunction(); + } + else { + var params = querystring.parse(location.hash.substring(1)); + addOpenHistoricalMapTimeSlider(map, params, originalLoadFunction); + } }; page.unload = function () { From 5842ba7ea22c697d6066d9451784decaa99d2d19 Mon Sep 17 00:00:00 2001 From: Rub21 Date: Fri, 10 Jul 2026 12:31:34 -0500 Subject: [PATCH 3/4] Share date helpers in OSM.Date --- app/assets/javascripts/index.js | 1 + app/assets/javascripts/index/date_utils.js | 60 ++++++++++++++++++++++ app/assets/javascripts/index/query.js | 32 +----------- app/assets/javascripts/index/search.js | 39 ++------------ 4 files changed, 66 insertions(+), 66 deletions(-) create mode 100644 app/assets/javascripts/index/date_utils.js diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index 7888c10a693..c0aa4cc3969 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -9,6 +9,7 @@ //= require leaflet.share //= require leaflet.query //= require index/contextmenu +//= require index/date_utils //= require index/search //= require index/layers/data //= require index/export diff --git a/app/assets/javascripts/index/date_utils.js b/app/assets/javascripts/index/date_utils.js new file mode 100644 index 00000000000..c8b2245e418 --- /dev/null +++ b/app/assets/javascripts/index/date_utils.js @@ -0,0 +1,60 @@ +// Shared date helpers for the map UI, kept in one place so query.js and +// search.js do not each carry their own copy. +OSM.Date = { + // Compare ISO 8601 dates correctly (handles BCE dates where string comparison fails) + compareDates: function (date1, date2) { + if (!date1 && !date2) return 0; + if (!date1) return -1; + if (!date2) return 1; + + const match1 = date1.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); + const match2 = date2.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); + if (!match1 || !match2) return date1.localeCompare(date2); + + const [, year1Str, month1Str, day1Str] = match1; + const [, year2Str, month2Str, day2Str] = match2; + const year1Int = parseInt(year1Str, 10); + const year2Int = parseInt(year2Str, 10); + + if (year1Int !== year2Int) return year1Int - year2Int; + + const month1 = month1Str ? parseInt(month1Str, 10) : 1; + const month2 = month2Str ? parseInt(month2Str, 10) : 1; + if (month1 !== month2) return month1 - month2; + + const day1 = day1Str ? parseInt(day1Str, 10) : 1; + const day2 = day2Str ? parseInt(day2Str, 10) : 1; + return day1 - day2; + }, + + // Days in a month, accounting for leap years (BCE has no year 0). + daysInMonth: function (year, month) { + if (month !== 2) return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]; + const y = year > 0 ? year : year + 1; + const leap = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0); + return leap ? 29 : 28; + }, + + // Fill a partial date to a full YYYY-MM-DD, at the start or end of its period. + padDate: function (value, startend) { + if (value === null || value === undefined || value === "") return null; + const date = String(value).trim().replace(/^\+/, ""); + if (/^-?\d+-\d\d-\d\d$/.test(date)) return date; + const yearMonth = date.match(/^(-?\d+)-(\d\d)$/); + if (yearMonth) { + if (startend === "start") return `${date}-01`; + const lastDay = OSM.Date.daysInMonth(parseInt(yearMonth[1], 10), parseInt(yearMonth[2], 10)); + return `${date}-${String(lastDay).padStart(2, "0")}`; + } + if (/^-?\d+$/.test(date)) return startend === "start" ? `${date}-01-01` : `${date}-12-31`; + return null; + }, + + // True when a feature with these dates exists at the given time. The end date + // pads to the last day of its period, so a feature ending in 1700 lasts all year. + existsAt: function (startDate, endDate, atDate) { + const afterStart = !startDate || OSM.Date.compareDates(OSM.Date.padDate(startDate, "start"), atDate) <= 0; + const beforeEnd = !endDate || OSM.Date.compareDates(OSM.Date.padDate(endDate, "end"), atDate) > 0; + return afterStart && beforeEnd; + } +}; diff --git a/app/assets/javascripts/index/query.js b/app/assets/javascripts/index/query.js index 12e9e4f5de3..3d84803cba5 100644 --- a/app/assets/javascripts/index/query.js +++ b/app/assets/javascripts/index/query.js @@ -320,41 +320,11 @@ OSM.Query = function (map) { return match ? parseInt(match[1], 10) < 1000 : false; } - // Compare ISO 8601 dates correctly (handles BCE dates where string comparison fails) - function compareDates(date1, date2) { - if (!date1 && !date2) return 0; - if (!date1) return -1; - if (!date2) return 1; - - const match1 = date1.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); - const match2 = date2.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); - if (!match1 || !match2) return date1.localeCompare(date2); - - const [, year1Str, month1Str, day1Str] = match1; - const [, year2Str, month2Str, day2Str] = match2; - const year1Int = parseInt(year1Str, 10); - const year2Int = parseInt(year2Str, 10); - - if (year1Int !== year2Int) return year1Int - year2Int; - - const month1 = month1Str ? parseInt(month1Str, 10) : 1; - const month2 = month2Str ? parseInt(month2Str, 10) : 1; - if (month1 !== month2) return month1 - month2; - - const day1 = day1Str ? parseInt(day1Str, 10) : 1; - const day2 = day2Str ? parseInt(day2Str, 10) : 1; - return day1 - day2; - } - function filterByDate(elements, currentDate) { if (!currentDate) return elements; return elements.filter(element => { const tags = element.tags || {}; - const startDate = tags.start_date; - const endDate = tags.end_date; - if (startDate && compareDates(startDate, currentDate) > 0) return false; - if (endDate && compareDates(endDate, currentDate) <= 0) return false; - return true; + return OSM.Date.existsAt(tags.start_date, tags.end_date, currentDate); }); } diff --git a/app/assets/javascripts/index/search.js b/app/assets/javascripts/index/search.js index 656aaeea930..d8897c1af91 100644 --- a/app/assets/javascripts/index/search.js +++ b/app/assets/javascripts/index/search.js @@ -91,48 +91,17 @@ OSM.Search = function (map) { } } - // Compare two ISO 8601 dates, tolerating partial (year, year-month) and BCE values. - // Same logic as compareDates() in query.js; kept local until we share a date util. - function compareDates(date1, date2) { - const match1 = date1.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); - const match2 = date2.match(/^(-?\d+)(?:-(\d{1,2}))?(?:-(\d{1,2}))?/); - if (!match1 || !match2) return date1.localeCompare(date2); - - const [, year1, month1, day1] = match1; - const [, year2, month2, day2] = match2; - if (parseInt(year1, 10) !== parseInt(year2, 10)) return parseInt(year1, 10) - parseInt(year2, 10); - - const m1 = month1 ? parseInt(month1, 10) : 1; - const m2 = month2 ? parseInt(month2, 10) : 1; - if (m1 !== m2) return m1 - m2; - - return (day1 ? parseInt(day1, 10) : 1) - (day2 ? parseInt(day2, 10) : 1); - } - - // The slider only accepts full YYYY-MM-DD, so pad a year or year-month before setDate(). - function padDate(value) { - if (value === null || value === undefined || value === "") return null; - const parts = String(value).trim().match(/^(-?\d{1,4})(?:-(\d{2}))?(?:-(\d{2}))?/); - if (!parts) return null; - return `${parts[1]}-${parts[2] || "01"}-${parts[3] || "01"}`; - } - // Move the slider so the picked feature is visible. If it already exists at the // current time we leave the slider alone. function adjustTimeSliderToResult(data) { if (!map.timeslider) return; + if (!data.startDate && !data.endDate) return; - const startDate = data.startDate != null && data.startDate !== "" ? String(data.startDate) : null; - const endDate = data.endDate != null && data.endDate !== "" ? String(data.endDate) : null; - if (!startDate && !endDate) return; - - const current = map.timeslider.getDate(); - const afterStart = !startDate || compareDates(startDate, current) <= 0; - const beforeEnd = !endDate || compareDates(endDate, current) > 0; - if (afterStart && beforeEnd) return; // already visible now + // leave the slider alone if the feature already exists at the current time + if (OSM.Date.existsAt(data.startDate, data.endDate, map.timeslider.getDate())) return; // out of view: jump to start_date, or to end_date when there is no start - const target = padDate(startDate || endDate); + const target = OSM.Date.padDate(data.startDate || data.endDate, "start"); if (target) map.timeslider.setDate(target); } From bfe7286d8c2fcdc5a005108d968d2ca12f477a65 Mon Sep 17 00:00:00 2001 From: Rub21 Date: Tue, 14 Jul 2026 19:35:42 -0500 Subject: [PATCH 4/4] Fix restore streamlined page load in search after the PR #327 --- app/assets/javascripts/index/search.js | 39 +++++++++----------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/index/search.js b/app/assets/javascripts/index/search.js index d8897c1af91..f0b8401cd31 100644 --- a/app/assets/javascripts/index/search.js +++ b/app/assets/javascripts/index/search.js @@ -131,34 +131,23 @@ OSM.Search = function (map) { }; page.load = function () { - // the original page.load content is the function below, and is used when one visits this page, be it first load OR later routing change - // below, we wrap "if map.timeslider" so we only try to add the timeslider if we don't already have it - function originalLoadFunction () { - $(".search_results_entry[data-href]").each(function (index) { - const entry = $(this); - fetchReplace(this.dataset, entry.children().first()) - .then(() => { - // go to first result of first geocoder - if (index === 0) { - const firstResult = entry.find("*[data-lat][data-lon]:first").first(); - if (firstResult.length) { - panToSearchResult(firstResult.data()); - } + $(".search_results_entry[data-href]").each(function (index) { + const entry = $(this); + fetchReplace(this.dataset, entry.children().first()) + .then(() => { + // go to first result of first geocoder + if (index === 0) { + const firstResult = entry.find("*[data-lat][data-lon]:first").first(); + if (firstResult.length) { + panToSearchResult(firstResult.data()); } - }); - }); + } + }); + }); - return map.getState(); - } // end originalLoadFunction + addOpenHistoricalMapTimeSlider(map); - // "if map.timeslider" only try to add the timeslider if we don't already have it - if (map.timeslider) { - originalLoadFunction(); - } - else { - var params = querystring.parse(location.hash.substring(1)); - addOpenHistoricalMapTimeSlider(map, params, originalLoadFunction); - } + return map.getState(); }; page.unload = function () {