Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RouteList): add location names #172

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default [
'ignoreRestSiblings': true,
},
],
'@typescript-eslint/no-redundant-type-constituents': 'off',
},
},
{
Expand Down
45 changes: 45 additions & 0 deletions src/map/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,53 @@ export interface ReverseGeocodingResponse extends FeatureCollection<Point, Rever
export type ReverseGeocodingFeature = ReverseGeocodingResponse['features'][number]

interface ReverseGeocodingFeatureProperties {
// mapbox_id: string
feature_type: 'country' | 'region' | 'postcode' | 'district' | 'place' | 'locality' | 'neighborhood' | 'street' | 'address'
name: string
name_preferred: string
place_formatted: string
full_address: string
context: ReverseGeocodingContextObject
// coordinates: {
// longitude: number
// latitude: number
// accuracy: 'rooftop' | 'parcel' | 'point' | 'interpolated' | 'approximate' | 'intersection'
// routable_points: {
// name: 'default' | string
// longitude: number
// latitude: number
// }[]
// }
// bbox?: [number, number, number, number]
// match_code: object
}

/**
* @see https://docs.mapbox.com/api/search/geocoding/#the-context-object
*/
interface ReverseGeocodingContextObject<S = ReverseGeocodingContextSubObject> {
country?: S & {
country_code: string
country_code_alpha_3: string
}
region?: S & {
region_code: string
region_code_full: string
}
postcode?: S
district?: S
place?: S
locality?: S
neighborhood?: S
street?: S
address?: S & {
address_number: string
street_name: string
}
}

interface ReverseGeocodingContextSubObject {
// mapbox_id: string
name: string
// wikidata_id?: string
}
43 changes: 42 additions & 1 deletion src/map/geocode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'

import { getFullAddress, reverseGeocode } from './geocode'
import { getFullAddress, getPlaceDetails, reverseGeocode } from './geocode'

describe('reverseGeocode', () => {
test('return null if coords are [0, 0]', async () => {
Expand All @@ -19,3 +19,44 @@ describe('getFullAddress', () => {
expect(await getFullAddress([-2.076843, 51.894799])).toBe('4 Montpellier Drive, Cheltenham, GL50 1TX, United Kingdom')
})
})

describe('getPlaceDetails', () => {
test('return null if coords are [0, 0]', async () => {
expect(await getPlaceDetails([0, 0])).toBeNull()
})

test('normal usage', async () => {
expect(await getPlaceDetails([-117.168638, 32.723695])).toEqual({
name: 'Little Italy',
details: 'San Diego, CA',
})
expect(await getPlaceDetails([-118.192757, 33.763015])).toEqual({
name: 'Downtown Long Beach',
details: 'Long Beach, CA',
})
expect(await getPlaceDetails([-74.003225, 40.714057])).toEqual({
name: 'Civic Center',
details: 'New York, NY',
})
expect(await getPlaceDetails([-0.113643, 51.504546])).toEqual({
name: 'Waterloo',
details: 'London',
})
expect(await getPlaceDetails([5.572254, 50.644280])).toEqual({
name: 'Liege',
details: 'Liège',
})
expect(await getPlaceDetails([-2.236802, 53.480931])).toEqual({
name: 'Northern Quarter',
details: 'Manchester',
})
expect(await getPlaceDetails([-8.626736, 52.663829])).toEqual({
name: 'Prior\'s-Land',
details: 'Limerick',
})
expect(await getPlaceDetails([-75.704956, 45.410103])).toEqual({
name: 'Centretown',
details: 'Ottawa, ON',
})
})
})
31 changes: 31 additions & 0 deletions src/map/geocode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { ReverseGeocodingResponse, ReverseGeocodingFeature } from '~/map/ap
import { MAPBOX_TOKEN } from '~/map/config'


const INCLUDE_REGION_CODE = ['US', 'CA']


export async function reverseGeocode(position: Position): Promise<ReverseGeocodingFeature | null> {
if (position[0] === 0 && position[1] === 0) {
return null
Expand Down Expand Up @@ -33,3 +36,31 @@ export async function getFullAddress(position: Position): Promise<string | null>
if (!feature) return null
return feature.properties.full_address
}


export async function getPlaceDetails(position: Position): Promise<{
name: string
details: string
} | null> {
const feature = await reverseGeocode(position)
if (!feature) return null
const { properties: { context } } = feature
const name = [
context.neighborhood?.name,
context.locality?.name,
context.place?.name,
context.district?.name,
].find(Boolean) || ''
let details = [
context.place?.name,
context.locality?.name,
context.district?.name,
].filter((it) => it !== name).find(Boolean) || ''
if (context.region?.region_code && INCLUDE_REGION_CODE.includes(context.country?.country_code || '')) {
details = details ? `${details}, ${context.region.region_code}` : context.region.region_code
}
return {
name,
details,
}
}
28 changes: 28 additions & 0 deletions src/pages/dashboard/components/RouteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ import Card, { CardContent, CardHeader } from '~/components/material/Card'
import RouteStatistics from '~/components/RouteStatistics'
import type { RouteSegments } from '~/types'
import { useDimensions } from '~/utils/window'
import { getPlaceDetails } from '~/map/geocode'
import Icon from '~/components/material/Icon'


interface RouteLocationProps {
route: RouteSegments
}

const RouteLocation: VoidComponent<RouteLocationProps> = (props) => {
const startPosition = () => [props.route.start_lng || 0, props.route.start_lat || 0] as number[]
const endPosition = () => [props.route.end_lng || 0, props.route.end_lat || 0] as number[]
const [startDetails] = createResource(startPosition, getPlaceDetails)
const [endDetails] = createResource(endPosition, getPlaceDetails)
return <div class="flex items-center gap-4">
<div>
<div>{startDetails()?.name}</div>
<div>{startDetails()?.details}</div>
</div>
<Icon>arrow_right_alt</Icon>
<div>
<div>{endDetails()?.name}</div>
<div>{endDetails()?.details}</div>
</div>
</div>
}


interface RouteCardProps {
Expand All @@ -35,6 +60,9 @@ const RouteCard: VoidComponent<RouteCardProps> = (props) => {
<CardHeader
headline={startTime().format('ddd, MMM D, YYYY')}
subhead={`${startTime().format('h:mm A')} to ${endTime().format('h:mm A')}`}
trailing={<Suspense fallback={<div class="skeleton-loader h-8 w-16" />}>
<RouteLocation route={props.route} />
</Suspense>}
/>

<CardContent>
Expand Down
2 changes: 2 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface Route extends ApiResponseBase {
procqcamera: number
procqlog: number
radar?: boolean
start_lat?: number
start_lng?: number
start_time: string
url: string
user_id: string | null
Expand Down