Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | 2x 2x 2x 316x 188x 186x 76x 318x 318x 318x 318x 318x 8x 157x 157x 157x 5x 8x 8x 8x 161x 161x 159x 159x 159x 159x 8x 157x 10x 8x 1x | export type Region = "na" | "sa" | "eu" | "af" | "as" | "oc";
export interface MemberCluster {
lat: number;
lng: number;
count: number;
region: Region;
}
export interface Stay {
from: [number, number];
to: [number, number];
hello: string;
}
interface MemberFeature {
geometry?: { type?: string; coordinates?: number[] } | null;
properties?: { count?: number } | null;
}
export interface MemberGeoJson {
features?: MemberFeature[];
}
// Size of the grid cells members are grouped into for the city-level clusters
const CELL_DEGREES = 4;
const MAX_CITY_CLUSTERS = 110;
// Example stays drawn as travel lines. Each greeting is deliberately in the destination's language.
export const STAYS: Stay[] = [
{ from: [38.72, -9.14], to: [52.52, 13.4], hello: "Willkommen!" },
{ from: [40.71, -74.0], to: [19.43, -99.13], hello: "¡Bienvenidos!" },
{ from: [-23.55, -46.63], to: [38.72, -9.14], hello: "Bem-vindo!" },
{ from: [48.86, 2.35], to: [-1.29, 36.82], hello: "Karibu!" },
{ from: [37.57, 126.98], to: [35.68, 139.69], hello: "ようこそ" },
{ from: [51.5, -0.13], to: [13.76, 100.5], hello: "Sawasdee!" },
{ from: [-33.87, 151.21], to: [-36.85, 174.76], hello: "Kia ora!" },
{ from: [43.65, -79.38], to: [41.39, 2.17], hello: "¡Bienvenido!" },
{ from: [52.23, 21.01], to: [41.01, 28.98], hello: "Hoş geldin!" },
{ from: [-34.6, -58.38], to: [-33.45, -70.67], hello: "¡Bienvenida!" },
{ from: [28.61, 77.21], to: [27.72, 85.32], hello: "Namaste!" },
];
function regionOf(lat: number, lng: number): Region {
if (lng < -30) return lat > 12 ? "na" : "sa";
if (lat < -10 && lng > 110) return "oc";
if (lng >= 52 || (lng >= 40 && lat > 20 && lat < 30)) return "as";
return lat >= 35 ? "eu" : "af";
}
interface Accumulator {
latSum: number;
lngSum: number;
count: number;
}
function add<K>(groups: Map<K, Accumulator>, key: K, lat: number, lng: number, weight: number) {
const group = groups.get(key) ?? { latSum: 0, lngSum: 0, count: 0 };
group.latSum += lat * weight;
group.lngSum += lng * weight;
group.count += weight;
groups.set(key, group);
}
function toClusters(groups: Map<string, Accumulator>): MemberCluster[] {
return [...groups.values()].map(({ latSum, lngSum, count }) => {
const lat = latSum / count;
const lng = lngSum / count;
return { lat, lng, count, region: regionOf(lat, lng) };
});
}
/**
* Groups member locations into city-sized clusters (the biggest ones only) and continent-sized regions.
* Features may already be clusters with a `count` property, like /geojson/clustered-users.
*/
export function clusterMembers(geojson: MemberGeoJson): { cities: MemberCluster[]; regions: MemberCluster[] } {
const cells = new Map<string, Accumulator>();
const regions = new Map<Region, Accumulator>();
for (const feature of geojson.features ?? []) {
const coordinates = feature.geometry?.type === "Point" ? feature.geometry.coordinates : undefined;
if (!coordinates || coordinates.length < 2) continue;
const [lng, lat] = coordinates;
const weight = typeof feature.properties?.count === "number" ? feature.properties.count : 1;
add(cells, `${Math.floor(lat / CELL_DEGREES)}:${Math.floor(lng / CELL_DEGREES)}`, lat, lng, weight);
add(regions, regionOf(lat, lng), lat, lng, weight);
}
const cities = toClusters(cells)
.sort((a, b) => b.count - a.count)
.slice(0, MAX_CITY_CLUSTERS);
// A region's centroid can fall outside its own bounds, so keep the region it was grouped under
const regionClusters = [...regions.entries()].map(([region, { latSum, lngSum, count }]) => ({
lat: latSum / count,
lng: lngSum / count,
count,
region,
}));
return { cities, regions: regionClusters };
}
export async function fetchMemberLocations(url: string): Promise<MemberGeoJson> {
const response = await fetch(url, { credentials: "include" });
if (!response.ok) {
throw new Error(`Couldn't load member locations (${response.status})`);
}
return response.json();
}
|