Skip to content

Commit d559a5d

Browse files
Convert algorithms to JS
1 parent 4402f84 commit d559a5d

File tree

4 files changed

+86
-42
lines changed

4 files changed

+86
-42
lines changed

src/components/MapPin.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { toLonLat } from 'ol/proj'
33
import { getOlContext } from '../ol/Map.svelte'
44
import fragment from '../stores/fragment'
5-
import { findPlace } from '../search/geocode'
5+
import { findPlace } from '../search/nearest'
66
import round from '../utils/round'
77
88
const { map } = getOlContext()

src/search/geocode.ts

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,45 @@
1-
// import initSearch, {search} from 'nz-search'
2-
3-
const osmUrl = 'https://nominatim.openstreetmap.org/search'
4-
const nzPlacesUrl = "https://nz-places.now.sh/api/search";
5-
6-
export interface GeocodeResult {
7-
lat: number;
8-
lon: number;
9-
name: string;
10-
type: string;
11-
}
12-
13-
let initPromise: Promise<typeof import('nz-search')>;
14-
function getSearch() {
15-
if (!initPromise) {
16-
initPromise = (async () => {
17-
const [places, searchApi] = await Promise.all([(await fetch('data/places.json')).arrayBuffer(), import('nz-search')])
18-
await searchApi.default()
19-
searchApi.load_data(new Uint8Array(places))
20-
return searchApi
21-
})()
1+
import { getPlaces, type Place } from './places';
2+
3+
const getMatch = (queryParts: string[], place: Place) => {
4+
const lowerName = place.name.toLowerCase();
5+
let rank = 0
6+
let lastMatchIndex: number | undefined = undefined
7+
8+
for (const part of queryParts) {
9+
const matchIndex = lowerName.indexOf(part)
10+
if (matchIndex === -1) return 0
11+
12+
// If this is the first part, rank it higher if its at the start than if its somewhere else.
13+
if (lastMatchIndex === undefined) {
14+
rank += (matchIndex === 0
15+
? 1
16+
: 1 / matchIndex)
17+
} else {
18+
const numerator = matchIndex <= lastMatchIndex ? 0.5 : 1
19+
rank += numerator / Math.abs(matchIndex - lastMatchIndex)
20+
}
2221
}
23-
return initPromise
22+
return rank
2423
}
2524

26-
const searchOsm = async (query: string): Promise<GeocodeResult[]> => {
27-
// Hackily include the country because the nominatim API is terrible.
28-
query = query + ", NZ";
29-
const results: GeocodeResult[] = await fetch(`${osmUrl}?q=${encodeURIComponent(query)}&format=jsonv2`)
30-
.then(r => r.json());
25+
const searchNzPlaces = async (query: string, maxResults = 100): Promise<Place[]> => {
26+
const lowerQuery = query.toLowerCase()
27+
const lowerQueryParts = lowerQuery.split(' ').filter(p => p)
3128

32-
for (const result of results) {
33-
result['name'] = result['display_name'];
34-
}
35-
return results;
36-
}
29+
const places = await getPlaces()
3730

38-
const searchNzPlaces = async (query: string): Promise<GeocodeResult[]> => {
39-
const api = await getSearch()
31+
const results: Place[] = []
32+
for (const place of places) {
33+
const match = getMatch(lowerQueryParts, place)
34+
if (match === 0) continue
4035

41-
const results = api.search(query, 100)
36+
results.push(place)
37+
if (results.length >= maxResults) break
38+
}
4239
return results
4340
}
4441

45-
export const findPlace = async (lat: number, lon: number) => {
46-
const search = await getSearch()
47-
return search.closest_place(lat, lon, 0.1)
48-
}
49-
50-
export default async (query: string, sources=[searchNzPlaces]): Promise<GeocodeResult[]> => {
42+
export default async (query: string, sources = [searchNzPlaces]): Promise<Place[]> => {
5143
const results = await Promise.all(sources.map(s => s(query)));
5244
return results.reduce((prev, next) => [...prev, ...next], [])
5345
}

src/search/nearest.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { getPlaces, type Place } from "./places";
2+
3+
const toRadians = (degrees: number) => degrees * Math.PI / 180
4+
5+
export const getDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => {
6+
const r = 6371.0; // radius of the earth in kilometers
7+
const dLat = toRadians(lat2 - lat1);
8+
const lLon = toRadians(lon2 - lon1);
9+
const a = Math.sin(dLat / 2) ** 2
10+
+ Math.cos(toRadians(lat1))
11+
* Math.cos(toRadians(lat2))
12+
* Math.sin(lLon / 2) ** 2;
13+
const c = 2 * Math.asin(Math.sqrt(a));
14+
return r * c;
15+
}
16+
17+
export const closestPlace = (lat: number, lon: number, places: Place[], maxDistance: number) => {
18+
let minDistance = -1
19+
let closest: Place | undefined
20+
21+
for (const place of places) {
22+
const distance = getDistance(lat, lon, place.lat, place.lon)
23+
if (distance < minDistance || !closest) {
24+
minDistance = distance
25+
closest = place
26+
}
27+
}
28+
29+
if (minDistance < maxDistance) {
30+
return closest
31+
}
32+
return undefined
33+
}
34+
35+
export const findPlace = async (lat: number, lon: number) => closestPlace(lat, lon, await getPlaces(), 0.1)

src/search/places.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const url = '/data/places.json'
2+
3+
export interface Place {
4+
name: string,
5+
lat: number,
6+
lon: number,
7+
type: string
8+
}
9+
10+
let placesPromise: Promise<Place[]>;
11+
12+
export const getPlaces = () => {
13+
if (!placesPromise) {
14+
placesPromise = fetch(url).then(r => r.json())
15+
}
16+
return placesPromise
17+
}

0 commit comments

Comments
 (0)