@konce-pt/map 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/cluster.d.ts +41 -0
- package/dist/cluster.js +110 -0
- package/dist/countries.d.ts +27 -0
- package/dist/countries.js +50 -0
- package/dist/engine.d.ts +84 -0
- package/dist/engine.js +3 -0
- package/dist/engines/google.engine.d.ts +51 -0
- package/dist/engines/google.engine.js +396 -0
- package/dist/engines/leaflet.engine.d.ts +13 -0
- package/dist/engines/leaflet.engine.js +232 -0
- package/dist/format.d.ts +13 -0
- package/dist/format.js +32 -0
- package/dist/geo.d.ts +64 -0
- package/dist/geo.js +136 -0
- package/dist/hours.d.ts +41 -0
- package/dist/hours.js +174 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +16 -0
- package/dist/marker-html.d.ts +37 -0
- package/dist/marker-html.js +119 -0
- package/dist/types.d.ts +238 -0
- package/dist/types.js +8 -0
- package/dist/url.d.ts +8 -0
- package/dist/url.js +20 -0
- package/package.json +55 -0
package/dist/geo.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { KptLatLng, KptMapBounds, KptMapPoint } from './types.ts';
|
|
2
|
+
/** Bok kafla w pikselach — stała obu silników. */
|
|
3
|
+
export declare const KPT_TILE_SIZE = 256;
|
|
4
|
+
/**
|
|
5
|
+
* Granica szerokości w Mercatorze. Powyżej niej projekcja ucieka do nieskończoności,
|
|
6
|
+
* więc obie biblioteki przycinają świat dokładnie w tym miejscu.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KPT_MAX_LATITUDE = 85.05112877980659;
|
|
9
|
+
/** Promień Ziemi w metrach (sfera, jak w Leaflet `L.CRS.Earth`). */
|
|
10
|
+
export declare const KPT_EARTH_RADIUS = 6378137;
|
|
11
|
+
/** Przycina szerokość do zakresu obsługiwanego przez Mercator. */
|
|
12
|
+
export declare function clampLat(lat: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* Zawija długość geograficzną do zakresu [-180, 180].
|
|
15
|
+
*
|
|
16
|
+
* Wartości już w zakresie wracają nietknięte — także -180, bo w projekcji jest to
|
|
17
|
+
* lewa krawędź świata (x = 0), a nie ta sama kolumna pikseli co +180. Zawijanie
|
|
18
|
+
* dotyczy wyłącznie tego, co wychodzi poza zakres: 540° to ten sam południk co 180°.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeLng(lng: number): number;
|
|
21
|
+
/** Liczba pikseli świata na danym zoomie. */
|
|
22
|
+
export declare function worldSize(zoom: number): number;
|
|
23
|
+
/** Współrzędne → piksele świata na danym zoomie (origin w lewym górnym rogu). */
|
|
24
|
+
export declare function project(at: KptLatLng, zoom: number): KptMapPoint;
|
|
25
|
+
/** Piksele świata → współrzędne. Odwrotność `project`. */
|
|
26
|
+
export declare function unproject(point: KptMapPoint, zoom: number): KptLatLng;
|
|
27
|
+
/** Najmniejszy prostokąt obejmujący wszystkie punkty. Pusta lista → `null`. */
|
|
28
|
+
export declare function boundsOf(points: readonly KptLatLng[]): KptMapBounds | null;
|
|
29
|
+
/**
|
|
30
|
+
* Najmniejszy zasięg w stopniach, przy którym `fitBounds` ma jeszcze sens (~100 m).
|
|
31
|
+
*
|
|
32
|
+
* Geokodery obrysowują pojedynczy adres prostokątem rzędu 0,0001°. Wpasowanie takiego
|
|
33
|
+
* w kadr wjeżdża na górny limit zoomu, a wynik zależy od tego, jak dostawca obrysował
|
|
34
|
+
* budynek — dwa sąsiednie adresy lądują w różnych przybliżeniach.
|
|
35
|
+
*/
|
|
36
|
+
export declare const KPT_MIN_FIT_SPAN = 0.001;
|
|
37
|
+
/**
|
|
38
|
+
* Czy prostokąt warto wpasowywać w kadr, zamiast potraktować go jak punkt.
|
|
39
|
+
*
|
|
40
|
+
* `false` dla zasięgu zdegenerowanego albo ciaśniejszego niż `minSpan` w OBU osiach —
|
|
41
|
+
* długa, wąska ulica ma jeden wymiar mikroskopijny i dalej zasługuje na `fitBounds`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isFittableBounds(bounds: KptMapBounds, minSpan?: number): boolean;
|
|
44
|
+
/** Środek prostokąta. */
|
|
45
|
+
export declare function boundsCenter(bounds: KptMapBounds): KptLatLng;
|
|
46
|
+
/** Czy punkt leży w prostokącie (granice włącznie). */
|
|
47
|
+
export declare function boundsContain(bounds: KptMapBounds, at: KptLatLng): boolean;
|
|
48
|
+
/** Rozszerza prostokąt o zadaną liczbę stopni w każdą stronę. */
|
|
49
|
+
export declare function padBounds(bounds: KptMapBounds, degrees: number): KptMapBounds;
|
|
50
|
+
/** Odległość po wielkim kole w metrach. */
|
|
51
|
+
export declare function haversine(a: KptLatLng, b: KptLatLng): number;
|
|
52
|
+
/**
|
|
53
|
+
* Ile metrów przypada na piksel na danej szerokości i zoomie.
|
|
54
|
+
* Potrzebne, żeby krąg dokładności geolokalizacji miał realny promień.
|
|
55
|
+
*/
|
|
56
|
+
export declare function metersPerPixel(lat: number, zoom: number): number;
|
|
57
|
+
/**
|
|
58
|
+
* Największy zoom, przy którym `bounds` mieści się w kontenerze o zadanym rozmiarze.
|
|
59
|
+
* Zwraca liczbę zmiennoprzecinkową — zaokrąglenie w dół zostawiamy wołającemu.
|
|
60
|
+
*/
|
|
61
|
+
export declare function zoomForBounds(bounds: KptMapBounds, size: {
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
}, paddingPx?: number): number;
|
package/dist/geo.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Bok kafla w pikselach — stała obu silników. */
|
|
2
|
+
export const KPT_TILE_SIZE = 256;
|
|
3
|
+
/**
|
|
4
|
+
* Granica szerokości w Mercatorze. Powyżej niej projekcja ucieka do nieskończoności,
|
|
5
|
+
* więc obie biblioteki przycinają świat dokładnie w tym miejscu.
|
|
6
|
+
*/
|
|
7
|
+
export const KPT_MAX_LATITUDE = 85.051_128_779_806_59;
|
|
8
|
+
/** Promień Ziemi w metrach (sfera, jak w Leaflet `L.CRS.Earth`). */
|
|
9
|
+
export const KPT_EARTH_RADIUS = 6_378_137;
|
|
10
|
+
/** Przycina szerokość do zakresu obsługiwanego przez Mercator. */
|
|
11
|
+
export function clampLat(lat) {
|
|
12
|
+
return Math.max(-KPT_MAX_LATITUDE, Math.min(KPT_MAX_LATITUDE, lat));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Zawija długość geograficzną do zakresu [-180, 180].
|
|
16
|
+
*
|
|
17
|
+
* Wartości już w zakresie wracają nietknięte — także -180, bo w projekcji jest to
|
|
18
|
+
* lewa krawędź świata (x = 0), a nie ta sama kolumna pikseli co +180. Zawijanie
|
|
19
|
+
* dotyczy wyłącznie tego, co wychodzi poza zakres: 540° to ten sam południk co 180°.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeLng(lng) {
|
|
22
|
+
if (lng >= -180 && lng <= 180)
|
|
23
|
+
return lng;
|
|
24
|
+
const wrapped = ((lng + 180) % 360 + 360) % 360 - 180;
|
|
25
|
+
return wrapped === -180 ? 180 : wrapped;
|
|
26
|
+
}
|
|
27
|
+
/** Liczba pikseli świata na danym zoomie. */
|
|
28
|
+
export function worldSize(zoom) {
|
|
29
|
+
return KPT_TILE_SIZE * Math.pow(2, zoom);
|
|
30
|
+
}
|
|
31
|
+
/** Współrzędne → piksele świata na danym zoomie (origin w lewym górnym rogu). */
|
|
32
|
+
export function project(at, zoom) {
|
|
33
|
+
const size = worldSize(zoom);
|
|
34
|
+
const lat = clampLat(at.lat);
|
|
35
|
+
const sin = Math.sin((lat * Math.PI) / 180);
|
|
36
|
+
return {
|
|
37
|
+
x: size * (normalizeLng(at.lng) / 360 + 0.5),
|
|
38
|
+
y: size * (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Piksele świata → współrzędne. Odwrotność `project`. */
|
|
42
|
+
export function unproject(point, zoom) {
|
|
43
|
+
const size = worldSize(zoom);
|
|
44
|
+
const lng = (point.x / size - 0.5) * 360;
|
|
45
|
+
const n = Math.PI * (1 - 2 * (point.y / size));
|
|
46
|
+
const lat = (180 / Math.PI) * Math.atan(Math.sinh(n));
|
|
47
|
+
return { lat, lng };
|
|
48
|
+
}
|
|
49
|
+
/** Najmniejszy prostokąt obejmujący wszystkie punkty. Pusta lista → `null`. */
|
|
50
|
+
export function boundsOf(points) {
|
|
51
|
+
if (points.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
let south = Infinity;
|
|
54
|
+
let north = -Infinity;
|
|
55
|
+
let west = Infinity;
|
|
56
|
+
let east = -Infinity;
|
|
57
|
+
for (const p of points) {
|
|
58
|
+
if (p.lat < south)
|
|
59
|
+
south = p.lat;
|
|
60
|
+
if (p.lat > north)
|
|
61
|
+
north = p.lat;
|
|
62
|
+
if (p.lng < west)
|
|
63
|
+
west = p.lng;
|
|
64
|
+
if (p.lng > east)
|
|
65
|
+
east = p.lng;
|
|
66
|
+
}
|
|
67
|
+
return { south, west, north, east };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Najmniejszy zasięg w stopniach, przy którym `fitBounds` ma jeszcze sens (~100 m).
|
|
71
|
+
*
|
|
72
|
+
* Geokodery obrysowują pojedynczy adres prostokątem rzędu 0,0001°. Wpasowanie takiego
|
|
73
|
+
* w kadr wjeżdża na górny limit zoomu, a wynik zależy od tego, jak dostawca obrysował
|
|
74
|
+
* budynek — dwa sąsiednie adresy lądują w różnych przybliżeniach.
|
|
75
|
+
*/
|
|
76
|
+
export const KPT_MIN_FIT_SPAN = 0.001;
|
|
77
|
+
/**
|
|
78
|
+
* Czy prostokąt warto wpasowywać w kadr, zamiast potraktować go jak punkt.
|
|
79
|
+
*
|
|
80
|
+
* `false` dla zasięgu zdegenerowanego albo ciaśniejszego niż `minSpan` w OBU osiach —
|
|
81
|
+
* długa, wąska ulica ma jeden wymiar mikroskopijny i dalej zasługuje na `fitBounds`.
|
|
82
|
+
*/
|
|
83
|
+
export function isFittableBounds(bounds, minSpan = KPT_MIN_FIT_SPAN) {
|
|
84
|
+
const lat = bounds.north - bounds.south;
|
|
85
|
+
const lng = bounds.east - bounds.west;
|
|
86
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng) || lat < 0 || lng < 0)
|
|
87
|
+
return false;
|
|
88
|
+
return lat >= minSpan || lng >= minSpan;
|
|
89
|
+
}
|
|
90
|
+
/** Środek prostokąta. */
|
|
91
|
+
export function boundsCenter(bounds) {
|
|
92
|
+
return { lat: (bounds.south + bounds.north) / 2, lng: (bounds.west + bounds.east) / 2 };
|
|
93
|
+
}
|
|
94
|
+
/** Czy punkt leży w prostokącie (granice włącznie). */
|
|
95
|
+
export function boundsContain(bounds, at) {
|
|
96
|
+
return at.lat >= bounds.south && at.lat <= bounds.north && at.lng >= bounds.west && at.lng <= bounds.east;
|
|
97
|
+
}
|
|
98
|
+
/** Rozszerza prostokąt o zadaną liczbę stopni w każdą stronę. */
|
|
99
|
+
export function padBounds(bounds, degrees) {
|
|
100
|
+
return {
|
|
101
|
+
south: clampLat(bounds.south - degrees),
|
|
102
|
+
north: clampLat(bounds.north + degrees),
|
|
103
|
+
west: bounds.west - degrees,
|
|
104
|
+
east: bounds.east + degrees,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Odległość po wielkim kole w metrach. */
|
|
108
|
+
export function haversine(a, b) {
|
|
109
|
+
const toRad = Math.PI / 180;
|
|
110
|
+
const dLat = (b.lat - a.lat) * toRad;
|
|
111
|
+
const dLng = (b.lng - a.lng) * toRad;
|
|
112
|
+
const s = Math.sin(dLat / 2) ** 2 +
|
|
113
|
+
Math.cos(a.lat * toRad) * Math.cos(b.lat * toRad) * Math.sin(dLng / 2) ** 2;
|
|
114
|
+
return 2 * KPT_EARTH_RADIUS * Math.asin(Math.min(1, Math.sqrt(s)));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Ile metrów przypada na piksel na danej szerokości i zoomie.
|
|
118
|
+
* Potrzebne, żeby krąg dokładności geolokalizacji miał realny promień.
|
|
119
|
+
*/
|
|
120
|
+
export function metersPerPixel(lat, zoom) {
|
|
121
|
+
return (Math.cos((clampLat(lat) * Math.PI) / 180) * 2 * Math.PI * KPT_EARTH_RADIUS) / worldSize(zoom);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Największy zoom, przy którym `bounds` mieści się w kontenerze o zadanym rozmiarze.
|
|
125
|
+
* Zwraca liczbę zmiennoprzecinkową — zaokrąglenie w dół zostawiamy wołającemu.
|
|
126
|
+
*/
|
|
127
|
+
export function zoomForBounds(bounds, size, paddingPx = 0) {
|
|
128
|
+
const w = Math.max(1, size.width - paddingPx * 2);
|
|
129
|
+
const h = Math.max(1, size.height - paddingPx * 2);
|
|
130
|
+
// Rozpiętość liczona na zoomie 0, potem skalowana logarytmicznie.
|
|
131
|
+
const sw = project({ lat: bounds.south, lng: bounds.west }, 0);
|
|
132
|
+
const ne = project({ lat: bounds.north, lng: bounds.east }, 0);
|
|
133
|
+
const dx = Math.max(1e-9, Math.abs(ne.x - sw.x));
|
|
134
|
+
const dy = Math.max(1e-9, Math.abs(sw.y - ne.y));
|
|
135
|
+
return Math.min(Math.log2(w / dx), Math.log2(h / dy));
|
|
136
|
+
}
|
package/dist/hours.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { KptMapOpeningHours, KptMapWeekday } from './types.ts';
|
|
2
|
+
/** Minuta doby dla `HH:MM`; `24:00` to 1440. `null`, gdy zapis jest nieprawidłowy. */
|
|
3
|
+
export declare function parseTime(value: string): number | null;
|
|
4
|
+
/** `HH:MM` z minuty doby. 1440 wraca jako `24:00`, nie `00:00`. */
|
|
5
|
+
export declare function formatTime(minutes: number): string;
|
|
6
|
+
/** Dzień tygodnia wg ISO (1 = poniedziałek) z natywnej daty. */
|
|
7
|
+
export declare function isoWeekday(date: Date): KptMapWeekday;
|
|
8
|
+
/**
|
|
9
|
+
* Czy miejsce jest otwarte w danym momencie.
|
|
10
|
+
*
|
|
11
|
+
* Sprawdza dwa dni: bieżący oraz poprzedni, bo przedział przez północ
|
|
12
|
+
* (`22:00 – 01:00`) obejmuje wczesne godziny dnia następnego.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isOpenAt(hours: KptMapOpeningHours | undefined, date: Date): boolean;
|
|
15
|
+
/** Czy rozkład to pełna doba przez siedem dni — wtedy karta pisze po prostu „24/7". */
|
|
16
|
+
export declare function isAlwaysOpen(hours: KptMapOpeningHours | undefined): boolean;
|
|
17
|
+
/** Najbliższa zmiana stanu otwarte/zamknięte albo `null`, gdy w ciągu tygodnia jej nie ma. */
|
|
18
|
+
export declare function nextChange(hours: KptMapOpeningHours | undefined, date: Date): {
|
|
19
|
+
open: boolean;
|
|
20
|
+
at: Date;
|
|
21
|
+
} | null;
|
|
22
|
+
/** Skrócona nazwa dnia z `Intl` — bez słownika, więc działa dla dowolnego locale. */
|
|
23
|
+
export declare function weekdayName(day: KptMapWeekday, locale: string): string;
|
|
24
|
+
/** Wiersz rozkładu: jeden dzień albo sklejony zakres dni o identycznych godzinach. */
|
|
25
|
+
export interface KptMapHoursGroup {
|
|
26
|
+
/** `pon.` albo `wt.–sob.`, wg locale. */
|
|
27
|
+
label: string;
|
|
28
|
+
days: KptMapWeekday[];
|
|
29
|
+
closed: boolean;
|
|
30
|
+
/** Sformatowane przedziały, np. `['11:00 – 24:00']`. Puste, gdy zamknięte. */
|
|
31
|
+
ranges: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Rozkład tygodnia z sąsiadującymi dniami sklejonymi w jeden wiersz.
|
|
35
|
+
*
|
|
36
|
+
* Niedziela i poniedziałek się nie sklejają, choć w tablicy sąsiadują — tydzień
|
|
37
|
+
* czyta się liniowo od poniedziałku i zawijanie byłoby mylące.
|
|
38
|
+
*/
|
|
39
|
+
export declare function groupWeek(hours: KptMapOpeningHours | undefined, locale: string, separator?: string): KptMapHoursGroup[];
|
|
40
|
+
/** Siedem dni oznaczonych jako zamknięte — wartość startowa pola `hours` w formularzu. */
|
|
41
|
+
export declare function emptyWeek(): KptMapOpeningHours;
|
package/dist/hours.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Godziny otwarcia — pure TS, bez Angulara i bez silnika mapowego.
|
|
3
|
+
* Ten plik przenosi się do portu React bez zmian.
|
|
4
|
+
*
|
|
5
|
+
* Umowa o czasie: `HH:MM` w czasie lokalnym miejsca, a porównania robimy
|
|
6
|
+
* na lokalnym czasie przeglądarki. Miejsce w innej strefie niż użytkownik
|
|
7
|
+
* pokaże więc status wg zegara użytkownika — to świadome uproszczenie,
|
|
8
|
+
* bo model nie niesie strefy. Kto potrzebuje inaczej, liczy status u siebie.
|
|
9
|
+
*/
|
|
10
|
+
/** Minuta doby dla `HH:MM`; `24:00` to 1440. `null`, gdy zapis jest nieprawidłowy. */
|
|
11
|
+
export function parseTime(value) {
|
|
12
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
const hours = Number(match[1]);
|
|
16
|
+
const minutes = Number(match[2]);
|
|
17
|
+
if (minutes > 59)
|
|
18
|
+
return null;
|
|
19
|
+
const total = hours * 60 + minutes;
|
|
20
|
+
return total > 1440 ? null : total;
|
|
21
|
+
}
|
|
22
|
+
/** `HH:MM` z minuty doby. 1440 wraca jako `24:00`, nie `00:00`. */
|
|
23
|
+
export function formatTime(minutes) {
|
|
24
|
+
const m = Math.max(0, Math.min(1440, Math.round(minutes)));
|
|
25
|
+
return `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
|
|
26
|
+
}
|
|
27
|
+
/** Dzień tygodnia wg ISO (1 = poniedziałek) z natywnej daty. */
|
|
28
|
+
export function isoWeekday(date) {
|
|
29
|
+
const day = date.getDay();
|
|
30
|
+
return (day === 0 ? 7 : day);
|
|
31
|
+
}
|
|
32
|
+
function previousDay(day) {
|
|
33
|
+
return (day === 1 ? 7 : day - 1);
|
|
34
|
+
}
|
|
35
|
+
function dayOf(hours, day) {
|
|
36
|
+
return hours.find((entry) => entry.day === day);
|
|
37
|
+
}
|
|
38
|
+
/** Przedział przechodzi przez północ, gdy kończy się nie później niż zaczyna. */
|
|
39
|
+
function crossesMidnight(range) {
|
|
40
|
+
const from = parseTime(range.from);
|
|
41
|
+
const to = parseTime(range.to);
|
|
42
|
+
return from !== null && to !== null && to <= from;
|
|
43
|
+
}
|
|
44
|
+
function usableRanges(entry) {
|
|
45
|
+
if (!entry || entry.closed)
|
|
46
|
+
return [];
|
|
47
|
+
return entry.ranges.filter((r) => parseTime(r.from) !== null && parseTime(r.to) !== null);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Czy miejsce jest otwarte w danym momencie.
|
|
51
|
+
*
|
|
52
|
+
* Sprawdza dwa dni: bieżący oraz poprzedni, bo przedział przez północ
|
|
53
|
+
* (`22:00 – 01:00`) obejmuje wczesne godziny dnia następnego.
|
|
54
|
+
*/
|
|
55
|
+
export function isOpenAt(hours, date) {
|
|
56
|
+
if (!hours || hours.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
const minute = date.getHours() * 60 + date.getMinutes();
|
|
59
|
+
const today = isoWeekday(date);
|
|
60
|
+
for (const range of usableRanges(dayOf(hours, today))) {
|
|
61
|
+
const from = parseTime(range.from);
|
|
62
|
+
const to = parseTime(range.to);
|
|
63
|
+
if (to > from ? minute >= from && minute < to : minute >= from)
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
for (const range of usableRanges(dayOf(hours, previousDay(today)))) {
|
|
67
|
+
if (!crossesMidnight(range))
|
|
68
|
+
continue;
|
|
69
|
+
if (minute < parseTime(range.to))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/** Czy rozkład to pełna doba przez siedem dni — wtedy karta pisze po prostu „24/7". */
|
|
75
|
+
export function isAlwaysOpen(hours) {
|
|
76
|
+
if (!hours)
|
|
77
|
+
return false;
|
|
78
|
+
for (let day = 1; day <= 7; day++) {
|
|
79
|
+
const ranges = usableRanges(dayOf(hours, day));
|
|
80
|
+
const full = ranges.some((r) => parseTime(r.from) === 0 && parseTime(r.to) === 1440);
|
|
81
|
+
if (!full)
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/** Najbliższa zmiana stanu otwarte/zamknięte albo `null`, gdy w ciągu tygodnia jej nie ma. */
|
|
87
|
+
export function nextChange(hours, date) {
|
|
88
|
+
if (!hours || hours.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
const now = isOpenAt(hours, date);
|
|
91
|
+
// Granice przedziałów na najbliższe osiem dób; pierwsza różnica wygrywa.
|
|
92
|
+
for (let offset = 0; offset <= 8; offset++) {
|
|
93
|
+
const base = new Date(date);
|
|
94
|
+
base.setHours(0, 0, 0, 0);
|
|
95
|
+
base.setDate(base.getDate() + offset);
|
|
96
|
+
const day = isoWeekday(base);
|
|
97
|
+
const minutes = new Set();
|
|
98
|
+
for (const range of usableRanges(dayOf(hours, day))) {
|
|
99
|
+
minutes.add(parseTime(range.from));
|
|
100
|
+
const to = parseTime(range.to);
|
|
101
|
+
if (to > parseTime(range.from))
|
|
102
|
+
minutes.add(to);
|
|
103
|
+
}
|
|
104
|
+
for (const range of usableRanges(dayOf(hours, previousDay(day)))) {
|
|
105
|
+
if (crossesMidnight(range))
|
|
106
|
+
minutes.add(parseTime(range.to));
|
|
107
|
+
}
|
|
108
|
+
for (const minute of [...minutes].sort((a, b) => a - b)) {
|
|
109
|
+
const at = new Date(base);
|
|
110
|
+
at.setMinutes(minute);
|
|
111
|
+
if (at.getTime() <= date.getTime())
|
|
112
|
+
continue;
|
|
113
|
+
const open = isOpenAt(hours, at);
|
|
114
|
+
if (open !== now)
|
|
115
|
+
return { open, at };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/** Skrócona nazwa dnia z `Intl` — bez słownika, więc działa dla dowolnego locale. */
|
|
121
|
+
export function weekdayName(day, locale) {
|
|
122
|
+
// 2024-01-01 był poniedziałkiem, więc dzień ISO wypada na tę samą liczbę w miesiącu.
|
|
123
|
+
const date = new Date(Date.UTC(2024, 0, day));
|
|
124
|
+
return new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }).format(date);
|
|
125
|
+
}
|
|
126
|
+
function signatureOf(entry) {
|
|
127
|
+
const ranges = usableRanges(entry);
|
|
128
|
+
if (ranges.length === 0)
|
|
129
|
+
return 'closed';
|
|
130
|
+
return ranges.map((r) => `${parseTime(r.from)}-${parseTime(r.to)}`).join(',');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Rozkład tygodnia z sąsiadującymi dniami sklejonymi w jeden wiersz.
|
|
134
|
+
*
|
|
135
|
+
* Niedziela i poniedziałek się nie sklejają, choć w tablicy sąsiadują — tydzień
|
|
136
|
+
* czyta się liniowo od poniedziałku i zawijanie byłoby mylące.
|
|
137
|
+
*/
|
|
138
|
+
export function groupWeek(hours, locale, separator = ' – ') {
|
|
139
|
+
if (!hours || hours.length === 0)
|
|
140
|
+
return [];
|
|
141
|
+
const groups = [];
|
|
142
|
+
let signature = null;
|
|
143
|
+
for (let day = 1; day <= 7; day++) {
|
|
144
|
+
const entry = dayOf(hours, day);
|
|
145
|
+
const current = signatureOf(entry);
|
|
146
|
+
const previous = groups[groups.length - 1];
|
|
147
|
+
if (current === signature && previous) {
|
|
148
|
+
previous.days.push(day);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
signature = current;
|
|
152
|
+
const ranges = usableRanges(entry);
|
|
153
|
+
groups.push({
|
|
154
|
+
label: '',
|
|
155
|
+
days: [day],
|
|
156
|
+
closed: ranges.length === 0,
|
|
157
|
+
ranges: ranges.map((r) => `${r.from}${separator}${r.to}`),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
for (const group of groups) {
|
|
161
|
+
const first = weekdayName(group.days[0], locale);
|
|
162
|
+
const last = weekdayName(group.days[group.days.length - 1], locale);
|
|
163
|
+
group.label = group.days.length === 1 ? first : `${first}–${last}`;
|
|
164
|
+
}
|
|
165
|
+
return groups;
|
|
166
|
+
}
|
|
167
|
+
/** Siedem dni oznaczonych jako zamknięte — wartość startowa pola `hours` w formularzu. */
|
|
168
|
+
export function emptyWeek() {
|
|
169
|
+
return Array.from({ length: 7 }, (_, i) => ({
|
|
170
|
+
day: (i + 1),
|
|
171
|
+
closed: true,
|
|
172
|
+
ranges: [],
|
|
173
|
+
}));
|
|
174
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './types.ts';
|
|
2
|
+
export * from './geo.ts';
|
|
3
|
+
export * from './cluster.ts';
|
|
4
|
+
export * from './countries.ts';
|
|
5
|
+
export * from './marker-html.ts';
|
|
6
|
+
export * from './engine.ts';
|
|
7
|
+
export * from './hours.ts';
|
|
8
|
+
export * from './format.ts';
|
|
9
|
+
export * from './url.ts';
|
|
10
|
+
export * from './engines/leaflet.engine.ts';
|
|
11
|
+
export * from './engines/google.engine.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Rdzeń mapy — pure TS, bez frameworka. Obok obliczeń (geometria, klastrowanie, godziny)
|
|
3
|
+
* mieszkają tu adaptery silników: Leaflet i Google. Oba rozmawiają z resztą kodu wyłącznie
|
|
4
|
+
* przez interfejs `KptMapEngine`, więc port Angulara i Reacta dzielą całą tę warstwę.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./types.js";
|
|
7
|
+
export * from "./geo.js";
|
|
8
|
+
export * from "./cluster.js";
|
|
9
|
+
export * from "./countries.js";
|
|
10
|
+
export * from "./marker-html.js";
|
|
11
|
+
export * from "./engine.js";
|
|
12
|
+
export * from "./hours.js";
|
|
13
|
+
export * from "./format.js";
|
|
14
|
+
export * from "./url.js";
|
|
15
|
+
export * from "./engines/leaflet.engine.js";
|
|
16
|
+
export * from "./engines/google.engine.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { KptMapColor, KptMapMarkerShape } from './types.ts';
|
|
2
|
+
/** Rola koloru → zmienna CSS. Gotowe `var(--kpt-*)` przechodzi bez zmian; cokolwiek spoza tego
|
|
3
|
+
* kontraktu dostaje bezpieczny fallback zamiast trafić surowo do wyjścia. */
|
|
4
|
+
export declare function colorVarOf(color: KptMapColor): string;
|
|
5
|
+
/** Kolor treści na tle `color` — dla ról znany, dla własnych zmiennych fallback na biel tokenową. */
|
|
6
|
+
export declare function contrastVarOf(color: KptMapColor): string;
|
|
7
|
+
export declare function foregroundVarOf(color: KptMapColor): string;
|
|
8
|
+
/** Ucieka znaki, które rozbiłyby atrybut albo wstrzyknęły znacznik. */
|
|
9
|
+
export declare function escapeHtml(value: string): string;
|
|
10
|
+
export interface KptMarkerHtmlInput {
|
|
11
|
+
shape?: KptMapMarkerShape;
|
|
12
|
+
color: KptMapColor;
|
|
13
|
+
/** Inner-SVG ikony (bez wrappera `<svg>`); bez niego znacznik nie ma glifu. */
|
|
14
|
+
iconSvgInner?: string;
|
|
15
|
+
/** Tekst dla czytnika ekranu — zwykle tytuł lokalizacji. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Znacznik wybrany (otwarty popup) — dostaje klasę modyfikującą. */
|
|
18
|
+
selected?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function renderMarkerHtml(input: KptMarkerHtmlInput): string;
|
|
21
|
+
export interface KptClusterHtmlInput {
|
|
22
|
+
count: number;
|
|
23
|
+
color: KptMapColor;
|
|
24
|
+
/** Gotowy tekst dla czytnika (i18n rozwiązane po stronie Angulara). */
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
/** Koło z licznikiem; rozmiar rośnie schodkowo z liczebnością grupy. */
|
|
28
|
+
export declare function renderClusterHtml(input: KptClusterHtmlInput): string;
|
|
29
|
+
/** Próg wielkości grupy — trzy stopnie wystarczą, żeby zobaczyć różnicę bez skalowania ciągłego. */
|
|
30
|
+
export declare function clusterTier(count: number): 'sm' | 'md' | 'lg';
|
|
31
|
+
/** Powyżej 999 licznik przestaje się mieścić — skracamy. */
|
|
32
|
+
export declare function formatCount(count: number): string;
|
|
33
|
+
/** Znacznik pozycji użytkownika — ikona podawana z zewnątrz, jak przy zwykłym znaczniku. */
|
|
34
|
+
export declare function renderUserMarkerHtml(input: {
|
|
35
|
+
label: string;
|
|
36
|
+
iconSvgInner?: string;
|
|
37
|
+
}): string;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const ROLE_VARS = {
|
|
2
|
+
primary: 'var(--kpt-color-primary)',
|
|
3
|
+
success: 'var(--kpt-color-success)',
|
|
4
|
+
warning: 'var(--kpt-color-warning)',
|
|
5
|
+
danger: 'var(--kpt-color-danger)',
|
|
6
|
+
info: 'var(--kpt-color-info)',
|
|
7
|
+
muted: 'var(--kpt-color-muted)',
|
|
8
|
+
};
|
|
9
|
+
const ROLE_CONTRAST_VARS = {
|
|
10
|
+
primary: 'var(--kpt-color-primary-contrast)',
|
|
11
|
+
success: 'var(--kpt-color-success-contrast)',
|
|
12
|
+
warning: 'var(--kpt-color-warning-contrast)',
|
|
13
|
+
danger: 'var(--kpt-color-danger-contrast)',
|
|
14
|
+
info: 'var(--kpt-color-info-contrast)',
|
|
15
|
+
muted: 'var(--kpt-color-on-muted)',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Kształt jedynego wejścia spoza znanej roli, jakie kontrakt `KptMapColor` dopuszcza —
|
|
19
|
+
* gotowy token `var(--kpt-*)`. Dane runtime (JSON/CMS) nie są tym kontraktem związane, więc
|
|
20
|
+
* zanim trafią do atrybutu `style="..."` budowanego konkatenacją stringów w `renderMarkerHtml`/
|
|
21
|
+
* `renderClusterHtml`, muszą zostać zweryfikowane — inaczej cudzysłów/`>` wyrywa się z atrybutu
|
|
22
|
+
* (HTML injection), a średnik wstrzykuje dodatkową deklarację CSS.
|
|
23
|
+
*/
|
|
24
|
+
const SAFE_VAR_TOKEN = /^var\(--kpt-[\w-]+\)$/;
|
|
25
|
+
/** Rola koloru → zmienna CSS. Gotowe `var(--kpt-*)` przechodzi bez zmian; cokolwiek spoza tego
|
|
26
|
+
* kontraktu dostaje bezpieczny fallback zamiast trafić surowo do wyjścia. */
|
|
27
|
+
export function colorVarOf(color) {
|
|
28
|
+
const known = ROLE_VARS[color];
|
|
29
|
+
if (known)
|
|
30
|
+
return known;
|
|
31
|
+
return SAFE_VAR_TOKEN.test(color) ? color : ROLE_VARS.muted;
|
|
32
|
+
}
|
|
33
|
+
/** Kolor treści na tle `color` — dla ról znany, dla własnych zmiennych fallback na biel tokenową. */
|
|
34
|
+
export function contrastVarOf(color) {
|
|
35
|
+
return ROLE_CONTRAST_VARS[color] ?? 'var(--kpt-color-text-inverse)';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Kolor **glifu stojącego wprost na powierzchni**, bez własnego tła.
|
|
39
|
+
*
|
|
40
|
+
* Różni się od `colorVarOf()` tylko dla roli `muted`: `--kpt-color-muted` to neutral-100,
|
|
41
|
+
* czyli token tła, a nie treści — goły znak w tym kolorze na jasnej powierzchni jest
|
|
42
|
+
* niewidoczny. Bierzemy więc czytelny szary tekstowy. Pozostałe role są nasycone
|
|
43
|
+
* i jako treść czytają się bez zmian.
|
|
44
|
+
*/
|
|
45
|
+
const ROLE_FOREGROUND_VARS = {
|
|
46
|
+
...ROLE_VARS,
|
|
47
|
+
muted: 'var(--kpt-color-on-surface-muted)',
|
|
48
|
+
};
|
|
49
|
+
export function foregroundVarOf(color) {
|
|
50
|
+
const known = ROLE_FOREGROUND_VARS[color];
|
|
51
|
+
if (known)
|
|
52
|
+
return known;
|
|
53
|
+
return SAFE_VAR_TOKEN.test(color) ? color : ROLE_FOREGROUND_VARS.muted;
|
|
54
|
+
}
|
|
55
|
+
/** Ucieka znaki, które rozbiłyby atrybut albo wstrzyknęły znacznik. */
|
|
56
|
+
export function escapeHtml(value) {
|
|
57
|
+
return value
|
|
58
|
+
.replaceAll('&', '&')
|
|
59
|
+
.replaceAll('<', '<')
|
|
60
|
+
.replaceAll('>', '>')
|
|
61
|
+
.replaceAll('"', '"')
|
|
62
|
+
.replaceAll("'", ''');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Pinezka o wysokości `--kpt-map-marker-size`, zakotwiczona czubkiem w punkcie.
|
|
66
|
+
* `viewBox` 24×32: 24 szerokości koła + ogon do 32.
|
|
67
|
+
*/
|
|
68
|
+
const PIN_PATH = 'M12 0C5.8 0 .8 5 .8 11.2c0 8 9.9 19.4 10.3 19.9a1.2 1.2 0 0 0 1.8 0c.4-.5 10.3-11.9 10.3-19.9C23.2 5 18.2 0 12 0z';
|
|
69
|
+
export function renderMarkerHtml(input) {
|
|
70
|
+
// `shape` trafia do atrybutu `class="..."` konkatenacją stringów — `KptMapMarkerShape` ogranicza
|
|
71
|
+
// to tylko compile-time, dane runtime (JSON/CMS) nie są tym związane (ten sam powód, dla którego
|
|
72
|
+
// `color`/`contrast` dostały escapowanie/whitelistę wyżej w pliku).
|
|
73
|
+
const shape = escapeHtml(input.shape ?? 'pin');
|
|
74
|
+
const color = colorVarOf(input.color);
|
|
75
|
+
const contrast = contrastVarOf(input.color);
|
|
76
|
+
const label = escapeHtml(input.label);
|
|
77
|
+
const cls = `kpt-map__marker kpt-map__marker--${shape}${input.selected ? ' kpt-map__marker--selected' : ''}`;
|
|
78
|
+
const glyph = input.iconSvgInner
|
|
79
|
+
? `<svg class="kpt-map__marker-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"` +
|
|
80
|
+
` stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${input.iconSvgInner}</svg>`
|
|
81
|
+
: '';
|
|
82
|
+
const body = shape === 'pin'
|
|
83
|
+
? `<svg class="kpt-map__marker-shape" viewBox="0 0 24 32" aria-hidden="true">` +
|
|
84
|
+
`<path d="${PIN_PATH}" /></svg><span class="kpt-map__marker-glyph">${glyph}</span>`
|
|
85
|
+
: `<span class="kpt-map__marker-glyph">${glyph}</span>`;
|
|
86
|
+
return (`<div class="${cls}" role="img" aria-label="${label}" title="${label}"` +
|
|
87
|
+
` style="--_kpt-map-marker-bg:${color};--_kpt-map-marker-fg:${contrast}">${body}</div>`);
|
|
88
|
+
}
|
|
89
|
+
/** Koło z licznikiem; rozmiar rośnie schodkowo z liczebnością grupy. */
|
|
90
|
+
export function renderClusterHtml(input) {
|
|
91
|
+
const color = colorVarOf(input.color);
|
|
92
|
+
const contrast = contrastVarOf(input.color);
|
|
93
|
+
const label = escapeHtml(input.label);
|
|
94
|
+
const tier = clusterTier(input.count);
|
|
95
|
+
return (`<div class="kpt-map__cluster kpt-map__cluster--${tier}" role="img" aria-label="${label}" title="${label}"` +
|
|
96
|
+
` style="--_kpt-map-cluster-bg:${color};--_kpt-map-cluster-fg:${contrast}">` +
|
|
97
|
+
`<span class="kpt-map__cluster-count">${formatCount(input.count)}</span></div>`);
|
|
98
|
+
}
|
|
99
|
+
/** Próg wielkości grupy — trzy stopnie wystarczą, żeby zobaczyć różnicę bez skalowania ciągłego. */
|
|
100
|
+
export function clusterTier(count) {
|
|
101
|
+
if (count < 10)
|
|
102
|
+
return 'sm';
|
|
103
|
+
if (count < 100)
|
|
104
|
+
return 'md';
|
|
105
|
+
return 'lg';
|
|
106
|
+
}
|
|
107
|
+
/** Powyżej 999 licznik przestaje się mieścić — skracamy. */
|
|
108
|
+
export function formatCount(count) {
|
|
109
|
+
return count > 999 ? '999+' : String(count);
|
|
110
|
+
}
|
|
111
|
+
/** Znacznik pozycji użytkownika — ikona podawana z zewnątrz, jak przy zwykłym znaczniku. */
|
|
112
|
+
export function renderUserMarkerHtml(input) {
|
|
113
|
+
const label = escapeHtml(input.label);
|
|
114
|
+
const glyph = input.iconSvgInner
|
|
115
|
+
? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"` +
|
|
116
|
+
` stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${input.iconSvgInner}</svg>`
|
|
117
|
+
: '';
|
|
118
|
+
return `<div class="kpt-map__user-marker" role="img" aria-label="${label}" title="${label}">${glyph}</div>`;
|
|
119
|
+
}
|