fix: realtime improvements

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-20 09:52:29 +01:00
parent d1b39c4c93
commit 88a2d876ce
20 changed files with 2060 additions and 536 deletions

View File

@@ -0,0 +1,298 @@
import type { Coordinate, CoordinateCluster } from './coordinates';
import { getClusterDetailLevel } from './coordinates';
import type { DisplayMarker } from './map-types';
export const PILL_W = 220;
export const PILL_H = 32;
export const ANCHOR_R = 3;
export const PILL_GAP = 6;
const COUNTRY_CODE_PATTERN = /^[A-Z]{2}$/;
const regionDisplayNames =
typeof Intl !== 'undefined'
? new Intl.DisplayNames(['en'], { type: 'region' })
: null;
export function normalizeLocationValue(value?: string) {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
export function isValidCoordinate(coordinate: Coordinate) {
return Number.isFinite(coordinate.lat) && Number.isFinite(coordinate.long);
}
export function getCoordinateIdentity(coordinate: Coordinate) {
return [
normalizeLocationValue(coordinate.country) ?? '',
normalizeLocationValue(coordinate.city) ?? '',
isValidCoordinate(coordinate) ? coordinate.long.toFixed(4) : 'invalid-long',
isValidCoordinate(coordinate) ? coordinate.lat.toFixed(4) : 'invalid-lat',
].join(':');
}
export function getDisplayMarkerId(members: Coordinate[]) {
const validMembers = members.filter(isValidCoordinate);
if (validMembers.length === 0) {
return 'invalid-cluster';
}
return validMembers.map(getCoordinateIdentity).sort().join('|');
}
export function getWeightedScreenPoint(
markers: Array<{
count: number;
screenPoint: {
x: number;
y: number;
};
}>
) {
let weightedX = 0;
let weightedY = 0;
let totalWeight = 0;
for (const marker of markers) {
weightedX += marker.screenPoint.x * marker.count;
weightedY += marker.screenPoint.y * marker.count;
totalWeight += marker.count;
}
return {
x: weightedX / totalWeight,
y: weightedY / totalWeight,
};
}
export function formatCountryLabel(country?: string) {
const normalized = normalizeLocationValue(country);
if (!normalized) {
return undefined;
}
if (!COUNTRY_CODE_PATTERN.test(normalized)) {
return normalized;
}
return regionDisplayNames?.of(normalized) ?? normalized;
}
export function summarizeLocation(members: Coordinate[]) {
const cities = new Set<string>();
const countries = new Set<string>();
for (const member of members) {
const city = normalizeLocationValue(member.city);
const country = normalizeLocationValue(member.country);
if (city) {
cities.add(city);
}
if (country) {
countries.add(country);
}
}
return {
cityCount: cities.size,
countryCount: countries.size,
firstCity: [...cities][0],
firstCountry: [...countries][0],
};
}
export function createDisplayLabel(
marker: CoordinateCluster,
zoom: number
): string {
const detailLevel = getClusterDetailLevel(zoom);
if (detailLevel === 'country') {
return (
formatCountryLabel(marker.location.country) ?? marker.location.city ?? '?'
);
}
if (detailLevel === 'city') {
return (
marker.location.city ?? formatCountryLabel(marker.location.country) ?? '?'
);
}
const cityMember = marker.members.find((member) => member.city?.trim());
return (
cityMember?.city?.trim() ??
formatCountryLabel(marker.location.country) ??
'?'
);
}
export function getDetailQueryScope(
marker: CoordinateCluster,
zoom: number
): DisplayMarker['detailScope'] {
const detailLevel = getClusterDetailLevel(zoom);
if (detailLevel === 'country') {
return 'country';
}
if (detailLevel === 'city') {
return marker.location.city ? 'city' : 'country';
}
return 'coordinate';
}
export function getMergedDetailQueryScope(
zoom: number
): DisplayMarker['detailScope'] {
const detailLevel = getClusterDetailLevel(zoom);
return detailLevel === 'country' ? 'country' : 'city';
}
export function createMergedDisplayLabel(
marker: CoordinateCluster,
zoom: number
): string {
const detailLevel = getClusterDetailLevel(zoom);
const summary = summarizeLocation(marker.members);
if (detailLevel === 'country') {
if (summary.countryCount <= 1) {
return (
formatCountryLabel(summary.firstCountry) ?? summary.firstCity ?? '?'
);
}
return `${summary.countryCount} countries`;
}
if (detailLevel === 'city') {
if (summary.cityCount === 1 && summary.firstCity) {
return summary.firstCity;
}
if (summary.countryCount === 1) {
const country = formatCountryLabel(summary.firstCountry);
if (country && summary.cityCount > 1) {
return `${country}, ${summary.cityCount} cities`;
}
return country ?? `${summary.cityCount} places`;
}
if (summary.countryCount > 1) {
return `${summary.countryCount} countries`;
}
}
if (summary.cityCount === 1 && summary.firstCity) {
return summary.firstCity;
}
if (summary.countryCount === 1) {
const country = formatCountryLabel(summary.firstCountry);
if (country && summary.cityCount > 1) {
return `${country}, ${summary.cityCount} places`;
}
return country ?? `${marker.members.length} places`;
}
return `${Math.max(summary.countryCount, summary.cityCount, 2)} places`;
}
export function getBadgeOverlayPosition(
marker: DisplayMarker,
size: { width: number; height: number }
) {
const overlayWidth = Math.min(380, size.width - 24);
const preferredLeft = marker.screenPoint.x - overlayWidth / 2;
const left = Math.max(
12,
Math.min(preferredLeft, size.width - overlayWidth - 12)
);
const top = Math.max(
12,
Math.min(marker.screenPoint.y + 16, size.height - 340)
);
return { left, overlayWidth, top };
}
export function getProfileDisplayName(profile: {
firstName: string;
lastName: string;
email: string;
id: string;
}) {
const name = [profile.firstName, profile.lastName].filter(Boolean).join(' ');
return name || profile.email || profile.id;
}
export function getUniqueCoordinateDetailLocations(members: Coordinate[]) {
const locationsByKey: Record<
string,
{
city?: string;
country?: string;
lat: number;
long: number;
}
> = {};
for (const member of members) {
if (!isValidCoordinate(member)) {
continue;
}
const key = [
normalizeLocationValue(member.country) ?? '',
normalizeLocationValue(member.city) ?? '',
member.long.toFixed(4),
member.lat.toFixed(4),
].join(':');
locationsByKey[key] = {
city: member.city,
country: member.country,
lat: member.lat,
long: member.long,
};
}
return Object.values(locationsByKey);
}
export function getUniquePlaceDetailLocations(members: Coordinate[]) {
const locationsByKey: Record<
string,
{
city?: string;
country?: string;
}
> = {};
for (const member of members) {
const key = [
normalizeLocationValue(member.country) ?? '',
normalizeLocationValue(member.city) ?? '',
].join(':');
locationsByKey[key] = {
city: member.city,
country: member.country,
};
}
return Object.values(locationsByKey);
}