From 07a25faa6c60be0d2197646335defb95614ed21f Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 3 Feb 2024 23:00:31 -0500 Subject: [PATCH] use @rdub/next-leaflet, pnpm --- .github/workflows/www.yml | 13 ++- www/package.json | 2 +- www/pages/_app.js | 1 - www/pages/stations.tsx | 183 +++++-------------------------------- www/pnpm-lock.yaml | 58 ++++++++++++ www/src/station-circle.tsx | 55 +++++++++++ www/src/stations.tsx | 124 +++++++++++++++++++++++++ 7 files changed, 269 insertions(+), 167 deletions(-) create mode 100644 www/src/station-circle.tsx create mode 100644 www/src/stations.tsx diff --git a/.github/workflows/www.yml b/.github/workflows/www.yml index 96a77df7..2595f55c 100644 --- a/.github/workflows/www.yml +++ b/.github/workflows/www.yml @@ -15,16 +15,19 @@ jobs: ssh-key: ${{ secrets.WWW_DEPLOY_KEY }} - uses: actions/setup-node@v3 with: - node-version: 16 - cache: 'npm' - cache-dependency-path: www/package-lock.json + node-version: 19 + cache: 'pnpm' + cache-dependency-path: www/pnpm-lock.yaml + - uses: pnpm/action-setup@v2 + with: + version: 8 - name: Install working-directory: www - run: npm install + run: pnpm install - name: Build working-directory: www run: | - npm run export + pnpm run export touch out/.nojekyll - name: Serve Files uses: Eun/http-server-action@v1.0.6 diff --git a/www/package.json b/www/package.json index 8e5602df..f2fb386f 100644 --- a/www/package.json +++ b/www/package.json @@ -9,6 +9,7 @@ "@next/mdx": "^13.2.4", "@rdub/base": "^0.1.0", "@rdub/next-base": "^0.1.0", + "@rdub/next-leaflet": "0.1.0", "@rdub/next-markdown": "0.0.2", "@rdub/next-params": "^0.0.1", "@rdub/next-plotly": "^0.0.3", @@ -74,7 +75,6 @@ "export": "next build && next export", "serve": "http-server out", "start": "next start", - "install-remote": "npm-install-remote-only.sh", "screenshots": "node screenshots.js" }, "repository": { diff --git a/www/pages/_app.js b/www/pages/_app.js index 5bc925a1..51b8157e 100644 --- a/www/pages/_app.js +++ b/www/pages/_app.js @@ -1,5 +1,4 @@ import 'bootstrap/dist/css/bootstrap.css' -import 'leaflet/dist/leaflet.css'; import '../styles/globals.css' function MyApp({ Component, pageProps }) { diff --git a/www/pages/stations.tsx b/www/pages/stations.tsx index 4ed07aa9..b3493609 100644 --- a/www/pages/stations.tsx +++ b/www/pages/stations.tsx @@ -1,38 +1,26 @@ import Head from "../src/head" -import Map from '../src/components/Map'; -import {floatParam, LL, llParam, Param, ParsedParam, parseQueryParams, stringParam} from "@rdub/next-params/params"; -import {getSync, loadSync} from "@rdub/base/load" -import * as ReactLeaflet from "react-leaflet"; -import type L from 'leaflet'; -import {Dispatch, useMemo, useState} from 'react'; +import { floatParam, LL, llParam, Param, ParsedParam, parseQueryParams, stringParam } from "@rdub/next-params/params"; +import { getSync, loadSync } from "@rdub/base/load" +import { useMemo, useState } from 'react'; +import 'leaflet/dist/leaflet.css'; import css from './stations.module.css' import fetch from "node-fetch"; import _ from "lodash"; -import {LAST_MONTH_PATH} from "../src/paths"; +import { LAST_MONTH_PATH } from "../src/paths"; +import dynamic from "next/dynamic"; +import type { ID, Props, StationPairCounts, Stations } from "../src/stations"; +import type { MapContainerProps } from "@rdub/next-leaflet/container" + +const Map = dynamic(() => import("../src/stations"), { ssr: false }) const DEFAULT_CENTER = { lat: 40.758, lng: -73.965, } const DEFAULT_ZOOM = 12 const { entries, fromEntries, keys } = Object -const { sqrt } = Math - -type CountRow = { - id: string - count: number -} - -type StationValue = { - name: string - lat: number - lng: number - starts: number -} type Idx = string -type ID = string -type Stations = { [id: ID]: StationValue } type YmProps = { ym: string @@ -72,132 +60,6 @@ type ParsedParams = { ym: ParsedParam } -function getMetersPerPixel(map: L.Map) { - const centerLatLng = map.getCenter(); // get map center - const pointC = map.latLngToContainerPoint(centerLatLng); // convert to containerpoint (pixels) - const pointX: L.PointExpression = [pointC.x + 1, pointC.y]; // add one pixel to x - // const pointY: L.PointExpression = [pointC.x, pointC.y + 1]; // add one pixel to y - - // convert containerpoints to latlng's - const latLngC = map.containerPointToLatLng(pointC); - const latLngX = map.containerPointToLatLng(pointX); - // const latLngY = map.containerPointToLatLng(pointY); - - const distanceX = latLngC.distanceTo(latLngX); // calculate distance between c and x (latitude) - // const distanceY = latLngC.distanceTo(latLngY); // calculate distance between c and y (longitude) - - // const zoom = map.getZoom() - //console.log("distanceX:", distanceX, "distanceY:", distanceY, "center:", centerLatLng, "zoom:", zoom) - return distanceX -} - -function MapBody( - {TileLayer, Marker, Circle, CircleMarker, Polyline, Pane, Tooltip, useMapEvents, useMap }: typeof ReactLeaflet, - { setLL, setZoom, stations, selectedStationId, setSelectedStationId, stationPairCounts, url, attribution }: { - setLL: Dispatch - setZoom: Dispatch - stations: Stations - selectedStationId: string | undefined - setSelectedStationId: Dispatch - stationPairCounts: StationPairCounts | null - url: string - attribution: string - } -) { - // Error: No context provided: useLeafletContext() can only be used in a descendant of - const map = useMap() - const zoom = map.getZoom() - const mPerPx = useMemo(() => getMetersPerPixel(map), [ map, zoom, ]) - useMapEvents({ - moveend: () => setLL(map.getCenter()), - zoom: () => setZoom(map.getZoom()), - click: () => { setSelectedStationId(undefined) }, - }) - - const selectedStation = useMemo( - () => selectedStationId ? stations[selectedStationId] : undefined, - [ stations, selectedStationId ] - ) - - const lines = useMemo( - () => { - if (!selectedStation || !selectedStationId || !stationPairCounts) return null - if (!(selectedStationId in stationPairCounts)) { - console.log(`${selectedStationId} not found among ${keys(stationPairCounts).length} stations`) - return null - } - const selectedPairCounts = stationPairCounts[selectedStationId] - // console.log("selectedPairCounts:", selectedPairCounts) - const selectedPairValues = Array.from(Object.values(selectedPairCounts)) - // console.log("selectedPairValues:", selectedPairValues) - const maxDst = Math.max(...selectedPairValues) - const src = selectedStation - return { - entries(selectedPairCounts).map(([ id, count ]) => { - // if (Count != maxDst) return - if (!(id in stations)) { - console.log(`id ${id} not in stations:`, stations) - return - } - const {name, lat, lng} = stations[id] - return - - {src.name} → {name}: {count} - - - }) - } - - }, - [ stationPairCounts, selectedStationId, mPerPx ] - ) - - function StationCircle({ id, count, selected }: CountRow & { selected?: boolean }) { - if (!(id in stations)) { - console.log(`id ${id} not in stations`) - return null - } - const { name, lat, lng } = stations[id] - return { - if (id == selectedStationId) return - console.log("click:", name) - setSelectedStationId(id) - }, - mouseover: () => { - if (id == selectedStationId) return - console.log("over:", name) - setSelectedStationId(id) - }, - mousedown: () => { - if (id == selectedStationId) return - console.log("down:", name) - setSelectedStationId(id) - }, - }} - > - -

{name}: {count}

-
-
- } - - return <> - { - selectedStationId && selectedStation && - - } - {lines} - { - entries(stations).map(([ id, station ]) => ) - } - - -} - export function ymParam(init: string, push: boolean = true): Param { const ymRegex = /^20(\d{4})$/ const vRegex = /^\d{4}$/ @@ -226,10 +88,6 @@ export function ymParam(init: string, push: boolean = true): Param { } } -type StationPairCounts = { - [k1: string]: { [k2: string]: number } -} - export default function Home({ defaults }: { defaults: YmProps, }) { const params: Params = { ll: llParam({ init: DEFAULT_CENTER, places: 3, }), @@ -238,7 +96,7 @@ export default function Home({ defaults }: { defaults: YmProps, }) { ym: ymParam(defaults.ym), } const { - ll: [ { lat, lng }, setLL ], + ll: [ center, setCenter ], z: [ zoom, setZoom, ], ss: [ selectedStationId, setSelectedStationId ], ym: [ ym, setYM ], @@ -296,7 +154,17 @@ export default function Home({ defaults }: { defaults: YmProps, }) { const title = `Citi Bike rides by station, ${ymString}` - const { url, attribution } = MAPS['alidade_smooth_dark'] + const mapProps: MapContainerProps = { + center, setCenter, + zoom, setZoom, + className: css.homeMap, + } + const mapBodyProps: Props = { + stations, + selectedStationId, + setSelectedStationId, + stationPairCounts, + } return (
@@ -306,13 +174,8 @@ export default function Home({ defaults }: { defaults: YmProps, }) { path={`stations`} thumbnail={`ctbk-stations`} /> -
- { - (RL: typeof ReactLeaflet) =>
{ - MapBody(RL, { setLL, setZoom, stations, selectedStationId, setSelectedStationId, stationPairCounts, url, attribution, }) - }
- }
+ {title &&
{title}
}
diff --git a/www/pnpm-lock.yaml b/www/pnpm-lock.yaml index 01386e5b..7d040679 100644 --- a/www/pnpm-lock.yaml +++ b/www/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@rdub/next-base': specifier: ^0.1.0 version: 0.1.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + '@rdub/next-leaflet': + specifier: 0.1.0 + version: 0.1.0(leaflet@1.9.4)(next@14.1.0)(react-dom@18.2.0)(react-leaflet@4.2.1)(react@18.2.0) '@rdub/next-markdown': specifier: 0.0.2 version: 0.0.2(@babel/core@7.23.9)(@mdx-js/loader@2.3.0)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0) @@ -562,6 +565,39 @@ packages: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: false + /@fortawesome/fontawesome-common-types@6.5.1: + resolution: {integrity: sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + + /@fortawesome/fontawesome-svg-core@6.5.1: + resolution: {integrity: sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.5.1 + dev: false + + /@fortawesome/free-solid-svg-icons@6.5.1: + resolution: {integrity: sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.5.1 + dev: false + + /@fortawesome/react-fontawesome@0.2.0(@fortawesome/fontawesome-svg-core@6.5.1)(react@18.2.0): + resolution: {integrity: sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==} + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 + react: '>=16.3' + dependencies: + '@fortawesome/fontawesome-svg-core': 6.5.1 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -915,6 +951,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@rdub/next-leaflet@0.1.0(leaflet@1.9.4)(next@14.1.0)(react-dom@18.2.0)(react-leaflet@4.2.1)(react@18.2.0): + resolution: {integrity: sha512-RagenPhH0YNitnBYevmlc9Id8p+8M2iWBUSplbV6ogWbZ7wd9bS9mT4euIkaU2vrDUSIOGyVLotWHcO/D1wtVg==} + peerDependencies: + leaflet: ^1.9.4 + next: ^14 + react: ^18 + react-dom: ^18 + react-leaflet: ^4.2.1 + dependencies: + '@fortawesome/fontawesome-svg-core': 6.5.1 + '@fortawesome/free-solid-svg-icons': 6.5.1 + '@fortawesome/react-fontawesome': 0.2.0(@fortawesome/fontawesome-svg-core@6.5.1)(react@18.2.0) + '@rdub/next-base': 0.1.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) + '@vanilla-extract/css': 1.14.1 + leaflet: 1.9.4 + next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-leaflet: 4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) + dev: false + /@rdub/next-markdown@0.0.2(@babel/core@7.23.9)(@mdx-js/loader@2.3.0)(@types/react@18.2.52)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-qdENQbrNVjpO89H32Z+oUCNe5TLM4/5TltquwqtJUiXlUV4FQkXWX09UgEyjp0DaZKZTHl6wnnk+QFCck1DrDw==} peerDependencies: diff --git a/www/src/station-circle.tsx b/www/src/station-circle.tsx new file mode 100644 index 00000000..abbea937 --- /dev/null +++ b/www/src/station-circle.tsx @@ -0,0 +1,55 @@ +import { Dispatch } from "react"; +import { Circle, Tooltip } from "react-leaflet"; +import css from "../pages/stations.module.css"; +import { CountRow, Stations } from "./stations"; +const { sqrt } = Math + +export type Props = CountRow & { + stations: Stations + selectedStationId: string | undefined + setSelectedStationId: Dispatch + selected?: boolean +} + +export default function StationCircle( + { + id, + count, + selected, + selectedStationId, setSelectedStationId, + stations, + }: Props +) { + if (!(id in stations)) { + console.log(`id ${id} not in stations`) + return null + } + const { name, lat, lng } = stations[id] + return { + if (id == selectedStationId) return + console.log("click:", name) + setSelectedStationId(id) + }, + mouseover: () => { + if (id == selectedStationId) return + console.log("over:", name) + setSelectedStationId(id) + }, + mousedown: () => { + if (id == selectedStationId) return + console.log("down:", name) + setSelectedStationId(id) + }, + }} + > + +

{name}: {count}

+
+
+} diff --git a/www/src/stations.tsx b/www/src/stations.tsx new file mode 100644 index 00000000..98257bcb --- /dev/null +++ b/www/src/stations.tsx @@ -0,0 +1,124 @@ +import { Dispatch, useMemo } from "react" +import css from "../pages/stations.module.css" +import { Pane, Polyline, Tooltip, useMap, useMapEvents } from "react-leaflet" +import type { MapContainerProps } from "@rdub/next-leaflet/container" +import MapContainer from "@rdub/next-leaflet/container" +import { getMetersPerPixel } from "@rdub/next-leaflet/map/mPerPx" +import { entries, keys } from "@rdub/base/objs" +import StationCircle from "./station-circle"; + +const { sqrt } = Math + +export type ID = string +export type Stations = { [id: ID]: StationValue } + +export type StationPairCounts = { + [k1: string]: { [k2: string]: number } +} + +export type Props = { + stations: Stations + selectedStationId: string | undefined + setSelectedStationId: Dispatch + stationPairCounts: StationPairCounts | null + className?: string +} + +export type CountRow = { + id: string + count: number +} + +export type StationValue = { + name: string + lat: number + lng: number + starts: number +} + +export function MapBody( + { + stations, + selectedStationId, setSelectedStationId, + stationPairCounts, + }: Props +) { + const map = useMap() + const zoom = map.getZoom() + const mPerPx = useMemo(() => getMetersPerPixel(map), [ map, zoom, ]) + useMapEvents({ + click: () => { setSelectedStationId(undefined) }, + }) + + const selectedStation = useMemo( + () => selectedStationId ? stations[selectedStationId] : undefined, + [ stations, selectedStationId ] + ) + + const lines = useMemo( + () => { + if (!selectedStation || !selectedStationId || !stationPairCounts) return null + if (!(selectedStationId in stationPairCounts)) { + console.log(`${selectedStationId} not found among ${keys(stationPairCounts).length} stations`) + return null + } + const selectedPairCounts = stationPairCounts[selectedStationId] + // console.log("selectedPairCounts:", selectedPairCounts) + const selectedPairValues = Array.from(Object.values(selectedPairCounts)) + // console.log("selectedPairValues:", selectedPairValues) + const maxDst = Math.max(...selectedPairValues) + const src = selectedStation + return { + entries(selectedPairCounts).map(([ id, count ]) => { + // if (Count != maxDst) return + if (!(id in stations)) { + console.log(`id ${id} not in stations:`, stations) + return + } + const {name, lat, lng} = stations[id] + return + + {src.name} → {name}: {count} + + + }) + } + + }, + [ stationPairCounts, selectedStationId, mPerPx ] + ) + + const stationCircleProps = { stations, selectedStationId, setSelectedStationId } + return <> + { + selectedStationId && selectedStation && + + } + {lines} + { + entries(stations).map(([ id, station ]) => + ) + } + +} + +export default function Map({ mapProps, bodyProps }: { mapProps: MapContainerProps, bodyProps: Props }) { + return ( + + + + ) +}