From 91788dd54b15ed20afc26d7b23f1eddae6dd4804 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sat, 11 Jan 2025 21:26:33 -0500 Subject: [PATCH] added search --- YongeStreet.sln.DotSettings.user | 1 + YongeStreet/Controllers/HomeController.cs | 10 ++- YongeStreet/Services/DataService.cs | 69 ++++++++++++++----- YongeStreet/Views/Home/City.cshtml | 7 +- .../Views/Home/CityList_Partial.cshtml | 16 +++++ YongeStreet/Views/Home/Index.cshtml | 32 ++++++--- YongeStreet/Views/Shared/_Layout.cshtml | 31 +++------ YongeStreet/YongeStreet.csproj | 1 + YongeStreet/wwwroot/js/quebec-province-map.js | 40 +++++------ flake.nix | 34 +++++++++ nuget-deps.nix | 1 + 11 files changed, 169 insertions(+), 73 deletions(-) create mode 100644 YongeStreet/Views/Home/CityList_Partial.cshtml diff --git a/YongeStreet.sln.DotSettings.user b/YongeStreet.sln.DotSettings.user index a6ce5f8..1a66e59 100644 --- a/YongeStreet.sln.DotSettings.user +++ b/YongeStreet.sln.DotSettings.user @@ -2,4 +2,5 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded \ No newline at end of file diff --git a/YongeStreet/Controllers/HomeController.cs b/YongeStreet/Controllers/HomeController.cs index bc45fa5..bbe4453 100644 --- a/YongeStreet/Controllers/HomeController.cs +++ b/YongeStreet/Controllers/HomeController.cs @@ -7,7 +7,9 @@ namespace YongeStreet.Controllers; public record CityModel(string City, IList CityRoadLengthStats, DateOnly Date); -public record IndexModel(IList Cities); +public record IndexModel(List Cities); + +public record TmpModel(IList Cities); public class HomeController : Controller { @@ -33,6 +35,12 @@ public class HomeController : Controller return View(new IndexModel(cities)); } + public IActionResult CityList_Partial(string? search) + { + var cities = _dataService.GetCityOverview(search??""); + return View(new IndexModel(cities)); + } + public IActionResult Privacy() { return View(); diff --git a/YongeStreet/Services/DataService.cs b/YongeStreet/Services/DataService.cs index 42d06e5..b5bc2c3 100644 --- a/YongeStreet/Services/DataService.cs +++ b/YongeStreet/Services/DataService.cs @@ -1,4 +1,5 @@ using DuckDB.NET.Data; +using FuzzierSharp; namespace YongeStreet.Services; @@ -10,15 +11,27 @@ public record CityRoadLengthStats(string Name, double MunicipalRoadLength, DateO public class DataService { - private static readonly List CITY_STREET_HIGHWAY = ["primary", + private static readonly List CITY_STREET_HIGHWAY = + [ + "primary", "primary_link", - "secondary", + "secondary", "secondary_link", "tertiary", "tertiary_link", "residential", - "unclassified"]; - private readonly string path = System.Environment.GetEnvironmentVariable("PREPROCESSED_DUCKDB_FILE") ?? "/home/didier/all-dressed-programming/YongeStreet/real.duckdb"; + "unclassified" + ]; + + private readonly ILogger _logger; + + private readonly string path = Environment.GetEnvironmentVariable("PREPROCESSED_DUCKDB_FILE") ?? + "/home/didier/all-dressed-programming/YongeStreet/real.duckdb"; + + public DataService(ILogger logger) + { + _logger = logger; + } public IList GetRoads(string city, DateOnly extractDate) { @@ -48,20 +61,20 @@ public class DataService return ret; } - public IList GetCityOverview() + public List GetCityOverview() { using var duckDBConnection = new DuckDBConnection($"Data Source={path};ACCESS_MODE=READ_ONLY"); duckDBConnection.Open(); using var command = duckDBConnection.CreateCommand(); command.CommandText = """ - SELECT city_name, + WITH raw AS (SELECT city_name, SUM(length) FILTER (list_contains($allowed_highway, highway) AND extract_date = (Date '2025-01-01')) AS length, LIST(DISTINCT extract_date) AS available_years FROM city_road GROUP BY city_name - ORDER BY length desc - LIMIT 100 + ORDER BY length desc) + SELECT * FROM raw WHERE length > 1000 """; command.Parameters.Add(new DuckDBParameter("allowed_highway", CITY_STREET_HIGHWAY)); using var reader = command.ExecuteReader(); @@ -74,7 +87,36 @@ public class DataService return ret; } - + + public List GetAllCityNames() + { + using var duckDBConnection = new DuckDBConnection($"Data Source={path};ACCESS_MODE=READ_ONLY"); + duckDBConnection.Open(); + + using var command = duckDBConnection.CreateCommand(); + command.CommandText = "SELECT DISTINCT city_name FROM city_road"; + using var reader = command.ExecuteReader(); + + var ret = new List(); + while (reader.Read()) ret.Add(reader.GetString(0)); + return ret; + } + + public List GetCityOverview(string search) + { + _logger.LogWarning("Seeking " + search); + if (search == "") return GetCityOverview().Take(20).ToList(); + + var cityNames = GetAllCityNames(); + var foundCityNames = Process.ExtractSorted(search, cityNames); + return ( + from cityName in foundCityNames + join city in GetCityOverview() on cityName.Value equals city.Name + orderby cityName.Score descending + select city + ).Take(20).ToList(); + } + public IList GetCityRoadLengthStats(string city) { using var duckDBConnection = new DuckDBConnection($"Data Source={path};ACCESS_MODE=READ_ONLY"); @@ -95,15 +137,10 @@ public class DataService var ret = new List(); while (reader.Read()) - { - ret.Add(new CityRoadLengthStats(city, - reader.GetInt32(0), + ret.Add(new CityRoadLengthStats(city, + reader.GetInt32(0), reader.GetFieldValue(1))); - } return ret; } - - - } \ No newline at end of file diff --git a/YongeStreet/Views/Home/City.cshtml b/YongeStreet/Views/Home/City.cshtml index a9976cc..11b5920 100644 --- a/YongeStreet/Views/Home/City.cshtml +++ b/YongeStreet/Views/Home/City.cshtml @@ -12,7 +12,8 @@
+ data-city="@Model.City" + data-date="@Model.Date.ToString("yyyy-MM-dd")">
@@ -50,4 +51,6 @@
- \ No newline at end of file + + + diff --git a/YongeStreet/Views/Home/CityList_Partial.cshtml b/YongeStreet/Views/Home/CityList_Partial.cshtml new file mode 100644 index 0000000..ae6ed18 --- /dev/null +++ b/YongeStreet/Views/Home/CityList_Partial.cshtml @@ -0,0 +1,16 @@ +@model YongeStreet.Controllers.IndexModel + +@{ + Layout = ""; +} + + + +@foreach(var city in Model.Cities) +{ + + @Html.ActionLink(city.Name, "City", "Home", new {City=city.Name}) + @((city.TotalStreetSize / 1000 ).ToString("N")) + @string.Join(", ", city.AvailableDates) + +} \ No newline at end of file diff --git a/YongeStreet/Views/Home/Index.cshtml b/YongeStreet/Views/Home/Index.cshtml index 99feff1..f21caf5 100644 --- a/YongeStreet/Views/Home/Index.cshtml +++ b/YongeStreet/Views/Home/Index.cshtml @@ -6,6 +6,22 @@ } +

+ Le Québec possède un reseau routier impréssionant. +

+ + +
+
+ +
+ +
+
+
@@ -15,16 +31,10 @@ - - @foreach(var city in Model.Cities) - { - - - - - - } - - +
@Html.ActionLink(city.Name, "City", "Home", new {City=city.Name})@((city.TotalStreetSize / 1000 ).ToString("N"))@string.Join(", ", city.AvailableDates)
\ No newline at end of file diff --git a/YongeStreet/Views/Shared/_Layout.cshtml b/YongeStreet/Views/Shared/_Layout.cshtml index 2c5810b..be56d24 100644 --- a/YongeStreet/Views/Shared/_Layout.cshtml +++ b/YongeStreet/Views/Shared/_Layout.cshtml @@ -14,38 +14,23 @@ -@* *@
@RenderBody() + +
+ +
+ + @await RenderSectionAsync("Scripts", required: false) diff --git a/YongeStreet/YongeStreet.csproj b/YongeStreet/YongeStreet.csproj index 52fe4cd..18e9437 100644 --- a/YongeStreet/YongeStreet.csproj +++ b/YongeStreet/YongeStreet.csproj @@ -8,6 +8,7 @@ + diff --git a/YongeStreet/wwwroot/js/quebec-province-map.js b/YongeStreet/wwwroot/js/quebec-province-map.js index efb3383..fa29de5 100644 --- a/YongeStreet/wwwroot/js/quebec-province-map.js +++ b/YongeStreet/wwwroot/js/quebec-province-map.js @@ -1,17 +1,14 @@ import leaflet from "https://esm.sh/leaflet@1.9.4"; -import * as chartjs from "https://esm.sh/chart.js@4.4.7"; - - const london = [51.505, -0.09]; const centerOfTheWorld = london; -function findBaryCenter(points){ +function findBaryCenter(points) { let lat = 0; let lon = 0; - for (const point of points){ + for (const point of points) { lat += point[0]; - lon += point[1]; + lon += point[1]; } return [lat / points.length, lon / points.length] } @@ -19,7 +16,7 @@ function findBaryCenter(points){ function getCenter(data) { if (!data) { return centerOfTheWorld; - } + } // good enough estimation return findBaryCenter(data.map(road => findBaryCenter(road.points))); } @@ -34,34 +31,37 @@ function addRoads(roads, map) { } } -function getActiveCity(){ +function getActiveCity() { const elm = document.getElementById("leaflet-map"); return elm.getAttribute('data-city'); - } -function getUrl(city){ - const params = {city: city} +function getActiveDate() { + const elm = document.getElementById("leaflet-map"); + return elm.getAttribute('data-date') ?? ""; +} + +function getUrl(city, date) { + const params = {city: city} + if (date.trim() !== "") { + params.date = date; + } return `/api/roads?${new URLSearchParams(params).toString()}` } - - - -async function renderLeafletMap(city){ - const resp = await fetch(getUrl(city)); +async function renderLeafletMap(city, date) { + const resp = await fetch(getUrl(city, date)); const roads = await resp.json(); const map = leaflet.map("leaflet-map").setView(getCenter(roads), 13); leaflet - .tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19 }) + .tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {maxZoom: 19}) .addTo(map); addRoads(roads, map); } - - const activeCity = getActiveCity(); -await Promise.all([renderLeafletMap(activeCity)]) +const activeDate = getActiveDate(); +await Promise.all([renderLeafletMap(activeCity, activeDate)]) diff --git a/flake.nix b/flake.nix index cf50c1b..c386492 100644 --- a/flake.nix +++ b/flake.nix @@ -106,6 +106,30 @@ geofabrik-base-url = "${static-server-url}/download.geofabrik.de/north-america/canada/"; + osm-quebec."2015" = pkgs.fetchurl { + url = "${geofabrik-base-url}quebec-150101.osm.pbf"; + hash = "sha256-nRZxVuiKUosqTDaI2fx8VCBuAp/l8ie1krUrsen4Jh4="; + }; + + osm-quebec."2016" = pkgs.fetchurl { + url = "${geofabrik-base-url}quebec-160101.osm.pbf"; + hash = "sha256-OMdII4R1+iEDT9tc1m8JG+Ed9UKMapaf37ceX0O91DM="; + }; + + osm-quebec."2017" = pkgs.fetchurl { + url = "${geofabrik-base-url}quebec-170101.osm.pbf"; + hash = "sha256-9OdzfrV2KaG2z1NJAAVJ0FzqoU/K+Lp50hlFc8nHfNs="; + }; + + osm-quebec."2018" = pkgs.fetchurl { + url = "${geofabrik-base-url}quebec-180101.osm.pbf"; + hash = "sha256-Qa1a7HtEkvmnirzn6sSekidPTxUr2ObGv/LxY5+cZu8="; + }; + + osm-quebec."2019" = pkgs.fetchurl { + url = "${geofabrik-base-url}quebec-190101.osm.pbf"; + hash = "sha256-iquV6nXJaRmb05SOX0X9NjqGRO404AXLEug3xtypEVE="; + }; osm-quebec."2020" = pkgs.fetchurl { url = "${geofabrik-base-url}quebec-200101.osm.pbf"; hash = "sha256-RYoz4Qo/xwiEVBoA4xRHHpt8kvNS/mHskJBmR4+3vQk="; @@ -155,6 +179,11 @@ buildPhase = ""; installPhase = '' mkdir -p $out + ln -s ${data-dir osm-quebec."2015"} $out/2015 + ln -s ${data-dir osm-quebec."2016"} $out/2016 + ln -s ${data-dir osm-quebec."2017"} $out/2017 + ln -s ${data-dir osm-quebec."2018"} $out/2018 + ln -s ${data-dir osm-quebec."2019"} $out/2019 ln -s ${data-dir osm-quebec."2020"} $out/2020 ln -s ${data-dir osm-quebec."2021"} $out/2021 ln -s ${data-dir osm-quebec."2022"} $out/2022 @@ -169,6 +198,11 @@ hash = "sha256-jZEkKg/D3MOVWprVGmsc/ev2fY6TVcN3tD3NH23B2h4="; }; + + # to create the ./nuget-deps.nix file, run the below + # two comments + # $ dotnet restore --packages out + # $ nuget-to-nix out > nuget-deps.nix web-server = pkgs.buildDotnetModule { pname = "YongeStreet"; version = "0.1.0"; diff --git a/nuget-deps.nix b/nuget-deps.nix index e800add..ed10948 100644 --- a/nuget-deps.nix +++ b/nuget-deps.nix @@ -2,6 +2,7 @@ (fetchNuGet { pname = "coverlet.collector"; version = "6.0.0"; hash = "sha256-IEmweTMapcPhFHpmJsPXfmMhravYOrWupgjeOvMmQ4o="; }) (fetchNuGet { pname = "DuckDB.NET.Bindings.Full"; version = "1.1.3"; hash = "sha256-OVmnUwydfBHmAvsjDoWH8x4p5AvIgXl4rC1E6d7Wdw8="; }) (fetchNuGet { pname = "DuckDB.NET.Data.Full"; version = "1.1.3"; hash = "sha256-CRBYs/bickkngbDzQbbYUu3yLtJiWS66QxeO1lm8CXY="; }) + (fetchNuGet { pname = "FuzzierSharp"; version = "3.0.1"; hash = "sha256-XO88548BCBI9rIDFpyMBd2ohgdhVnVoQlNLJeifCjYU="; }) (fetchNuGet { pname = "Htmx"; version = "1.8.1"; hash = "sha256-kzCnFpbYjh/C1Q49Vhzap6hKPSdi0fldjCtgDVPm4XM="; }) (fetchNuGet { pname = "Htmx.TagHelpers"; version = "1.8.1"; hash = "sha256-Vy6qoqoN91OuxeMLZUtJ5itfryxerqdZLNVOgvQBcls="; }) (fetchNuGet { pname = "Microsoft.CodeCoverage"; version = "17.8.0"; hash = "sha256-cv/wAXfTNS+RWEsHWNKqRDHC7LOQSSdFJ1a9cZuSfJw="; })