@konce-pt/react 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 +58 -0
- package/dist/accordion/accordion.d.ts +45 -0
- package/dist/accordion/accordion.js +69 -0
- package/dist/alert/alert.d.ts +25 -0
- package/dist/alert/alert.js +31 -0
- package/dist/app-shell/app-shell.d.ts +94 -0
- package/dist/app-shell/app-shell.js +105 -0
- package/dist/auth/auth-types.d.ts +44 -0
- package/dist/auth/auth-types.js +2 -0
- package/dist/auth/auth.d.ts +76 -0
- package/dist/auth/auth.js +184 -0
- package/dist/autocomplete/autocomplete.d.ts +18 -0
- package/dist/autocomplete/autocomplete.js +72 -0
- package/dist/avatar/avatar.d.ts +33 -0
- package/dist/avatar/avatar.js +30 -0
- package/dist/avatar-group/avatar-group.d.ts +21 -0
- package/dist/avatar-group/avatar-group.js +15 -0
- package/dist/badge/badge.d.ts +25 -0
- package/dist/badge/badge.js +14 -0
- package/dist/bottom-sheet/bottom-sheet.d.ts +21 -0
- package/dist/bottom-sheet/bottom-sheet.js +22 -0
- package/dist/breadcrumb/breadcrumb.d.ts +19 -0
- package/dist/breadcrumb/breadcrumb.js +20 -0
- package/dist/button/button.d.ts +27 -0
- package/dist/button/button.js +49 -0
- package/dist/button-group/button-group.d.ts +25 -0
- package/dist/button-group/button-group.js +30 -0
- package/dist/card/card.d.ts +66 -0
- package/dist/card/card.js +96 -0
- package/dist/carousel/carousel.d.ts +20 -0
- package/dist/carousel/carousel.js +24 -0
- package/dist/charts/chart-legend.d.ts +23 -0
- package/dist/charts/chart-legend.js +9 -0
- package/dist/charts/chart-tooltip.d.ts +21 -0
- package/dist/charts/chart-tooltip.js +13 -0
- package/dist/charts/chart.d.ts +18 -0
- package/dist/charts/chart.js +231 -0
- package/dist/charts-entry/public-api.d.ts +4 -0
- package/dist/charts-entry/public-api.js +7 -0
- package/dist/checkbox/checkbox.d.ts +33 -0
- package/dist/checkbox/checkbox.js +29 -0
- package/dist/chip/chip.d.ts +22 -0
- package/dist/chip/chip.js +16 -0
- package/dist/chips-input/chips-input.d.ts +25 -0
- package/dist/chips-input/chips-input.js +54 -0
- package/dist/clock/clock.d.ts +17 -0
- package/dist/clock/clock.js +57 -0
- package/dist/color-picker/color-picker.d.ts +23 -0
- package/dist/color-picker/color-picker.js +28 -0
- package/dist/confirm/confirm-container.d.ts +13 -0
- package/dist/confirm/confirm-container.js +26 -0
- package/dist/confirm/confirm-store.d.ts +29 -0
- package/dist/confirm/confirm-store.js +52 -0
- package/dist/context-menu/context-menu.d.ts +21 -0
- package/dist/context-menu/context-menu.js +36 -0
- package/dist/data-table/data-table-types.d.ts +67 -0
- package/dist/data-table/data-table-types.js +1 -0
- package/dist/data-table/data-table.d.ts +62 -0
- package/dist/data-table/data-table.js +453 -0
- package/dist/data-table/export-csv.d.ts +7 -0
- package/dist/data-table/export-csv.js +19 -0
- package/dist/data-view/data-view.d.ts +21 -0
- package/dist/data-view/data-view.js +25 -0
- package/dist/date-range/date-range.d.ts +30 -0
- package/dist/date-range/date-range.js +155 -0
- package/dist/datepicker/datepicker.d.ts +39 -0
- package/dist/datepicker/datepicker.js +202 -0
- package/dist/dialog/dialog.d.ts +35 -0
- package/dist/dialog/dialog.js +59 -0
- package/dist/divider/divider.d.ts +16 -0
- package/dist/divider/divider.js +13 -0
- package/dist/drawer/drawer.d.ts +25 -0
- package/dist/drawer/drawer.js +25 -0
- package/dist/empty/empty.d.ts +18 -0
- package/dist/empty/empty.js +14 -0
- package/dist/fab/fab.d.ts +22 -0
- package/dist/fab/fab.js +17 -0
- package/dist/fieldset/fieldset.d.ts +22 -0
- package/dist/fieldset/fieldset.js +24 -0
- package/dist/file-upload/file-upload.d.ts +22 -0
- package/dist/file-upload/file-upload.js +50 -0
- package/dist/form-field/form-field-context.d.ts +23 -0
- package/dist/form-field/form-field-context.js +17 -0
- package/dist/form-field/form-field.d.ts +39 -0
- package/dist/form-field/form-field.js +41 -0
- package/dist/galleria/galleria.d.ts +22 -0
- package/dist/galleria/galleria.js +21 -0
- package/dist/grid/col.d.ts +39 -0
- package/dist/grid/col.js +32 -0
- package/dist/grid/flex.d.ts +45 -0
- package/dist/grid/flex.js +35 -0
- package/dist/grid/grid.d.ts +47 -0
- package/dist/grid/grid.js +38 -0
- package/dist/grid/layout.d.ts +18 -0
- package/dist/grid/layout.js +32 -0
- package/dist/grid-entry/public-api.d.ts +6 -0
- package/dist/grid-entry/public-api.js +8 -0
- package/dist/i18n/i18n-context.d.ts +45 -0
- package/dist/i18n/i18n-context.js +65 -0
- package/dist/icon/icon-registry.d.ts +16 -0
- package/dist/icon/icon-registry.js +43 -0
- package/dist/icon/icon.d.ts +17 -0
- package/dist/icon/icon.js +23 -0
- package/dist/icon-button/icon-button.d.ts +23 -0
- package/dist/icon-button/icon-button.js +14 -0
- package/dist/icons-entry/public-api.d.ts +12 -0
- package/dist/icons-entry/public-api.js +20 -0
- package/dist/image/image.d.ts +17 -0
- package/dist/image/image.js +16 -0
- package/dist/input/input.d.ts +41 -0
- package/dist/input/input.js +36 -0
- package/dist/input-mask/input-mask.d.ts +19 -0
- package/dist/input-mask/input-mask.js +65 -0
- package/dist/input-number/input-number.d.ts +28 -0
- package/dist/input-number/input-number.js +52 -0
- package/dist/input-otp/input-otp.d.ts +21 -0
- package/dist/input-otp/input-otp.js +59 -0
- package/dist/internal/cn.d.ts +9 -0
- package/dist/internal/cn.js +19 -0
- package/dist/internal/compose-refs.d.ts +6 -0
- package/dist/internal/compose-refs.js +16 -0
- package/dist/internal/overlay-container.d.ts +3 -0
- package/dist/internal/overlay-container.js +19 -0
- package/dist/internal/overlay.d.ts +26 -0
- package/dist/internal/overlay.js +64 -0
- package/dist/internal/slot.d.ts +23 -0
- package/dist/internal/slot.js +49 -0
- package/dist/internal/types.d.ts +25 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/use-breakpoint.d.ts +18 -0
- package/dist/internal/use-breakpoint.js +52 -0
- package/dist/internal/use-controllable-state.d.ts +11 -0
- package/dist/internal/use-controllable-state.js +18 -0
- package/dist/internal/use-id.d.ts +9 -0
- package/dist/internal/use-id.js +13 -0
- package/dist/knob/knob.d.ts +27 -0
- package/dist/knob/knob.js +86 -0
- package/dist/listbox/listbox.d.ts +28 -0
- package/dist/listbox/listbox.js +43 -0
- package/dist/map/map-create-dialog.d.ts +21 -0
- package/dist/map/map-create-dialog.js +196 -0
- package/dist/map/map-hours-field.d.ts +22 -0
- package/dist/map/map-hours-field.js +90 -0
- package/dist/map/map-image-field.d.ts +24 -0
- package/dist/map/map-image-field.js +61 -0
- package/dist/map/map-legend.d.ts +29 -0
- package/dist/map/map-legend.js +41 -0
- package/dist/map/map-poi-card.d.ts +42 -0
- package/dist/map/map-poi-card.js +152 -0
- package/dist/map/map-search.d.ts +37 -0
- package/dist/map/map-search.js +201 -0
- package/dist/map/map.d.ts +113 -0
- package/dist/map/map.js +425 -0
- package/dist/map/ports.d.ts +76 -0
- package/dist/map/ports.js +174 -0
- package/dist/map-entry/public-api.d.ts +20 -0
- package/dist/map-entry/public-api.js +19 -0
- package/dist/megamenu/megamenu.d.ts +17 -0
- package/dist/megamenu/megamenu.js +28 -0
- package/dist/menu/menu-item.d.ts +33 -0
- package/dist/menu/menu-item.js +1 -0
- package/dist/menu/menu-rows.d.ts +9 -0
- package/dist/menu/menu-rows.js +15 -0
- package/dist/menu/menu.d.ts +22 -0
- package/dist/menu/menu.js +31 -0
- package/dist/menubar/menubar.d.ts +16 -0
- package/dist/menubar/menubar.js +39 -0
- package/dist/meter-group/meter-group.d.ts +20 -0
- package/dist/meter-group/meter-group.js +24 -0
- package/dist/order-list/order-list.d.ts +18 -0
- package/dist/order-list/order-list.js +37 -0
- package/dist/paginator/paginator.d.ts +29 -0
- package/dist/paginator/paginator.js +45 -0
- package/dist/panel/panel.d.ts +31 -0
- package/dist/panel/panel.js +36 -0
- package/dist/password/password.d.ts +28 -0
- package/dist/password/password.js +37 -0
- package/dist/pick-list/pick-list.d.ts +28 -0
- package/dist/pick-list/pick-list.js +65 -0
- package/dist/popover/popover.d.ts +23 -0
- package/dist/popover/popover.js +27 -0
- package/dist/progress/progress.d.ts +19 -0
- package/dist/progress/progress.js +21 -0
- package/dist/public-api.d.ts +181 -0
- package/dist/public-api.js +105 -0
- package/dist/radio-group/radio-group.d.ts +24 -0
- package/dist/radio-group/radio-group.js +23 -0
- package/dist/rating/rating.d.ts +22 -0
- package/dist/rating/rating.js +30 -0
- package/dist/rich-text/lists.d.ts +17 -0
- package/dist/rich-text/lists.js +144 -0
- package/dist/rich-text/rich-text.d.ts +28 -0
- package/dist/rich-text/rich-text.js +654 -0
- package/dist/rich-text/sanitize.d.ts +10 -0
- package/dist/rich-text/sanitize.js +85 -0
- package/dist/rich-text/tables.d.ts +44 -0
- package/dist/rich-text/tables.js +268 -0
- package/dist/roadmap/roadmap.d.ts +37 -0
- package/dist/roadmap/roadmap.js +271 -0
- package/dist/roadmap-entry/public-api.d.ts +4 -0
- package/dist/roadmap-entry/public-api.js +7 -0
- package/dist/scroll-top/scroll-top.d.ts +16 -0
- package/dist/scroll-top/scroll-top.js +38 -0
- package/dist/select/select.d.ts +37 -0
- package/dist/select/select.js +117 -0
- package/dist/shared/types.d.ts +8 -0
- package/dist/shared/types.js +1 -0
- package/dist/sidenav/sidenav.d.ts +26 -0
- package/dist/sidenav/sidenav.js +41 -0
- package/dist/skeleton/skeleton.d.ts +20 -0
- package/dist/skeleton/skeleton.js +15 -0
- package/dist/slider/slider.d.ts +28 -0
- package/dist/slider/slider.js +30 -0
- package/dist/speed-dial/speed-dial.d.ts +20 -0
- package/dist/speed-dial/speed-dial.js +23 -0
- package/dist/spinner/spinner.d.ts +15 -0
- package/dist/spinner/spinner.js +14 -0
- package/dist/split-button/split-button.d.ts +28 -0
- package/dist/split-button/split-button.js +34 -0
- package/dist/splitter/splitter.d.ts +39 -0
- package/dist/splitter/splitter.js +80 -0
- package/dist/stepper/stepper.d.ts +51 -0
- package/dist/stepper/stepper.js +71 -0
- package/dist/switch/switch.d.ts +39 -0
- package/dist/switch/switch.js +30 -0
- package/dist/switch-group/switch-group.d.ts +29 -0
- package/dist/switch-group/switch-group.js +47 -0
- package/dist/tabs/tabs.d.ts +38 -0
- package/dist/tabs/tabs.js +74 -0
- package/dist/textarea/textarea.d.ts +28 -0
- package/dist/textarea/textarea.js +44 -0
- package/dist/timeline/timeline.d.ts +19 -0
- package/dist/timeline/timeline.js +12 -0
- package/dist/toast/toast-container.d.ts +12 -0
- package/dist/toast/toast-container.js +23 -0
- package/dist/toast/toast-store.d.ts +26 -0
- package/dist/toast/toast-store.js +51 -0
- package/dist/toolbar/toolbar.d.ts +29 -0
- package/dist/toolbar/toolbar.js +24 -0
- package/dist/tooltip/tooltip.d.ts +26 -0
- package/dist/tooltip/tooltip.js +113 -0
- package/dist/tree/tree.d.ts +31 -0
- package/dist/tree/tree.js +62 -0
- package/package.json +91 -0
package/dist/map/map.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { KPT_MAP_DEFAULT_ATTRIBUTION, KPT_MAP_DEFAULT_TILE_URL, boundsOf, clusterLocations, countByCategory, haversine, isCluster, isSplittable, renderClusterHtml, renderMarkerHtml, renderUserMarkerHtml, resolveCountryView, } from '@konce-pt/map';
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { cn } from "../internal/cn.js";
|
|
5
|
+
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
|
+
import { getKptIcon } from "../icon/icon-registry.js";
|
|
8
|
+
import { KptDialog } from "../dialog/dialog.js";
|
|
9
|
+
import { KptMapCreateDialog } from "./map-create-dialog.js";
|
|
10
|
+
import { KptMapLegend } from "./map-legend.js";
|
|
11
|
+
import { KptMapSearch } from "./map-search.js";
|
|
12
|
+
/** Rozmiary ikon w pikselach — silnik potrzebuje ich do zakotwiczenia znacznika. */
|
|
13
|
+
const MARKER_SIZE = { width: 32, height: 42 };
|
|
14
|
+
const DOT_SIZE = { width: 24, height: 24 };
|
|
15
|
+
const CLUSTER_SIZE = { width: 44, height: 44 };
|
|
16
|
+
/** Ile miejsca zostawić wokół zasięgu przy `fitBounds`. */
|
|
17
|
+
const FIT_PADDING = 48;
|
|
18
|
+
/** Zapas na strzałkę i cień popupu przy sprawdzaniu, czy mieści się w kontenerze. */
|
|
19
|
+
const POPUP_MARGIN = 16;
|
|
20
|
+
/**
|
|
21
|
+
* Identyfikator tymczasowej pinezki wyszukiwarki. Nie należy do wpisów, więc kliknięcie
|
|
22
|
+
* w nią niczego nie otwiera.
|
|
23
|
+
*/
|
|
24
|
+
const SEARCH_MARKER_ID = 'kpt-map:search';
|
|
25
|
+
/** Brak silnika to najczęstsza pomyłka przy pierwszym użyciu — komunikat niesie instrukcję. */
|
|
26
|
+
const NO_ENGINE_ERROR = 'KptMap: brak silnika mapy. Podaj fabrykę przez prop `engine` — ' +
|
|
27
|
+
'`engine={() => createLeafletEngine()}` albo ' +
|
|
28
|
+
'`engine={() => createGoogleEngine({ apiKey, mapId })}` z `@konce-pt/react/map`.';
|
|
29
|
+
/**
|
|
30
|
+
* Mapa z konfigurowalnymi znacznikami: kategorie z kolorem i ikoną, grupowanie z licznikiem,
|
|
31
|
+
* popup szczegółów, legenda, wyszukiwarka adresów i dodawanie lokalizacji prawym przyciskiem.
|
|
32
|
+
*
|
|
33
|
+
* Silnik (Leaflet albo Google) przychodzi propem `engine` z `@konce-pt/map` — ta sama warstwa,
|
|
34
|
+
* na której stoi port Angulara. Leaflet ładuje się dynamicznym `import()`, więc aplikacja bez
|
|
35
|
+
* mapy go nie pobiera; wymaga też arkusza `leaflet/dist/leaflet.css`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* <KptMap
|
|
39
|
+
* engine={() => createLeafletEngine()}
|
|
40
|
+
* locations={locations}
|
|
41
|
+
* categories={categories}
|
|
42
|
+
* renderPopup={({ location, data }) => <strong>{location.title}</strong>}
|
|
43
|
+
* />
|
|
44
|
+
*/
|
|
45
|
+
export function KptMap({ locations, categories, country = 'PL', view = null, bounds = null, restrictToCountry = false, cluster = true, clusterOptions, clusterColorBy = 'none', legend = true, legendCounts = true, hiddenCategories, defaultHiddenCategories = [], onHiddenCategoriesChange, showUserLocation = false, followUser = false, geolocation = null, allowCreate = false, search = false, searchPlaceholder = '', searchOptions, searchZoom = 16, searchMarker = true, geocoder = null, popupLayout = 'plain', distanceFrom = null, height = '480px', tileUrl = null, attribution = null, engine, renderPopup, renderCreateForm, onLocationClick, onClusterClick, onLocationCreate, onSearchSelect, onViewChange, onUserPositionChange, onUserLocationError, locale: localeOverride, dictionary, className, style, ...rest }) {
|
|
46
|
+
const { t } = useKptMessages({ locale: localeOverride, dictionary });
|
|
47
|
+
const canvasRef = useRef(null);
|
|
48
|
+
const popupRef = useRef(null);
|
|
49
|
+
const engineRef = useRef(null);
|
|
50
|
+
/**
|
|
51
|
+
* Łańcuch montażu i demontażu tego kontenera. Silnik mapowy oznacza element jako zajęty
|
|
52
|
+
* na czas życia mapy, a w trybie ścisłym React montuje efekt dwa razy pod rząd — drugi
|
|
53
|
+
* montaż musi więc poczekać, aż pierwszy skończy sprzątać.
|
|
54
|
+
*/
|
|
55
|
+
const mountChain = useRef(Promise.resolve());
|
|
56
|
+
const [engineKind, setEngineKind] = useState(null);
|
|
57
|
+
const [ready, setReady] = useState(false);
|
|
58
|
+
/** Zoom trzymamy w stanie, bo od niego zależy wynik grupowania. */
|
|
59
|
+
const [zoom, setZoom] = useState(6);
|
|
60
|
+
/** Rośnie przy każdym ruchu mapy — wymusza przeliczenie pozycji nakładek. */
|
|
61
|
+
const [viewportTick, setViewportTick] = useState(0);
|
|
62
|
+
const [popup, setPopup] = useState(null);
|
|
63
|
+
const [contextTarget, setContextTarget] = useState(null);
|
|
64
|
+
const [createFor, setCreateFor] = useState(null);
|
|
65
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
66
|
+
const [userPosition, setUserPosition] = useState(null);
|
|
67
|
+
const [locationBlocked, setLocationBlocked] = useState(false);
|
|
68
|
+
/** Ostatni wybrany wynik wyszukiwania — źródło tymczasowej pinezki. */
|
|
69
|
+
const [searchPin, setSearchPin] = useState(null);
|
|
70
|
+
const [hidden, setHidden] = useControllableState({
|
|
71
|
+
value: hiddenCategories,
|
|
72
|
+
defaultValue: defaultHiddenCategories,
|
|
73
|
+
onChange: onHiddenCategoriesChange,
|
|
74
|
+
});
|
|
75
|
+
const categoryById = useMemo(() => new Map(categories.map((category) => [category.id, category])), [categories]);
|
|
76
|
+
/** Lokalizacje po odfiltrowaniu kategorii ukrytych w legendzie. */
|
|
77
|
+
const visibleLocations = useMemo(() => {
|
|
78
|
+
const hiddenSet = new Set(hidden);
|
|
79
|
+
return hiddenSet.size === 0 ? locations : locations.filter((l) => !hiddenSet.has(l.categoryId));
|
|
80
|
+
}, [locations, hidden]);
|
|
81
|
+
const counts = useMemo(() => (legendCounts ? countByCategory(locations) : null), [legendCounts, locations]);
|
|
82
|
+
/** Wynik grupowania dla aktualnego zoomu — grupy i punkty pojedyncze. */
|
|
83
|
+
const entries = useMemo(() => {
|
|
84
|
+
if (!cluster)
|
|
85
|
+
return [...visibleLocations];
|
|
86
|
+
return clusterLocations(visibleLocations, {
|
|
87
|
+
...clusterOptions,
|
|
88
|
+
zoom,
|
|
89
|
+
categoryOrder: categories.map((category) => category.id),
|
|
90
|
+
});
|
|
91
|
+
}, [cluster, visibleLocations, clusterOptions, zoom, categories]);
|
|
92
|
+
const restrictBounds = useCallback(() => (restrictToCountry ? (bounds ?? resolveCountryView(country).bounds) : null), [restrictToCountry, bounds, country]);
|
|
93
|
+
/** Znaczniki gotowe dla silnika. HTML powstaje w rdzeniu, poza Reactem. */
|
|
94
|
+
const markers = useMemo(() => {
|
|
95
|
+
const selectedId = popup?.location.id ?? null;
|
|
96
|
+
const rendered = entries.map((entry) => {
|
|
97
|
+
if (isCluster(entry)) {
|
|
98
|
+
const category = clusterColorBy === 'dominant' ? categoryById.get(entry.dominantCategoryId) : undefined;
|
|
99
|
+
return {
|
|
100
|
+
id: entry.id,
|
|
101
|
+
at: entry.at,
|
|
102
|
+
html: renderClusterHtml({
|
|
103
|
+
count: entry.count,
|
|
104
|
+
color: category?.color ?? 'primary',
|
|
105
|
+
label: t('map.cluster', { count: entry.count }),
|
|
106
|
+
}),
|
|
107
|
+
size: CLUSTER_SIZE,
|
|
108
|
+
anchor: { x: CLUSTER_SIZE.width / 2, y: CLUSTER_SIZE.height / 2 },
|
|
109
|
+
// Grupy nad pojedynczymi punktami — licznik ma być czytelny.
|
|
110
|
+
zIndex: 500,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const category = categoryById.get(entry.categoryId);
|
|
114
|
+
const shape = category?.shape ?? 'pin';
|
|
115
|
+
const size = shape === 'pin' ? MARKER_SIZE : DOT_SIZE;
|
|
116
|
+
return {
|
|
117
|
+
id: entry.id,
|
|
118
|
+
at: entry.at,
|
|
119
|
+
html: renderMarkerHtml({
|
|
120
|
+
shape,
|
|
121
|
+
color: category?.color ?? 'muted',
|
|
122
|
+
iconSvgInner: category?.icon ? getKptIcon(category.icon) : undefined,
|
|
123
|
+
label: entry.title,
|
|
124
|
+
selected: entry.id === selectedId,
|
|
125
|
+
}),
|
|
126
|
+
size,
|
|
127
|
+
// Pinezka kotwiczy się czubkiem, kropka i kwadrat środkiem.
|
|
128
|
+
anchor: shape === 'pin' ? { x: size.width / 2, y: size.height } : { x: size.width / 2, y: size.height / 2 },
|
|
129
|
+
zIndex: entry.id === selectedId ? 400 : 0,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
// Pinezka wyniku wyszukiwania idzie tą samą drogą co reszta znaczników, więc przesuwa się
|
|
133
|
+
// z mapą klatka po klatce i nie wymaga niczego od silnika.
|
|
134
|
+
const pin = searchMarker ? searchPin : null;
|
|
135
|
+
if (pin) {
|
|
136
|
+
rendered.push({
|
|
137
|
+
id: SEARCH_MARKER_ID,
|
|
138
|
+
at: pin.at,
|
|
139
|
+
html: renderMarkerHtml({
|
|
140
|
+
shape: 'pin',
|
|
141
|
+
color: 'info',
|
|
142
|
+
iconSvgInner: getKptIcon('map-pin'),
|
|
143
|
+
label: pin.label,
|
|
144
|
+
}),
|
|
145
|
+
size: MARKER_SIZE,
|
|
146
|
+
anchor: { x: MARKER_SIZE.width / 2, y: MARKER_SIZE.height },
|
|
147
|
+
// Nad grupami (500) — to na nią użytkownik właśnie kazał mapie polecieć.
|
|
148
|
+
zIndex: 600,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return rendered;
|
|
152
|
+
}, [entries, popup, clusterColorBy, categoryById, t, searchMarker, searchPin]);
|
|
153
|
+
/**
|
|
154
|
+
* Przesuwa mapę tak, żeby świeżo otwarty popup zmieścił się w kontenerze. Wymiarów popupu
|
|
155
|
+
* nie znamy przed renderem, więc mierzymy go po dograniu ramki.
|
|
156
|
+
*/
|
|
157
|
+
const autoPanToPopup = useCallback((state) => {
|
|
158
|
+
requestAnimationFrame(() => {
|
|
159
|
+
const map = engineRef.current;
|
|
160
|
+
const element = popupRef.current;
|
|
161
|
+
if (!map || !element)
|
|
162
|
+
return;
|
|
163
|
+
const anchor = map.project(state.location.at);
|
|
164
|
+
const size = map.getSize();
|
|
165
|
+
const rect = element.getBoundingClientRect();
|
|
166
|
+
// Popup kotwiczy się nad znacznikiem i jest wyśrodkowany w poziomie.
|
|
167
|
+
const top = anchor.y - rect.height - POPUP_MARGIN;
|
|
168
|
+
const left = anchor.x - rect.width / 2;
|
|
169
|
+
const right = anchor.x + rect.width / 2;
|
|
170
|
+
let dx = 0;
|
|
171
|
+
let dy = 0;
|
|
172
|
+
if (top < POPUP_MARGIN)
|
|
173
|
+
dy = top - POPUP_MARGIN;
|
|
174
|
+
if (left < POPUP_MARGIN)
|
|
175
|
+
dx = left - POPUP_MARGIN;
|
|
176
|
+
else if (right > size.width - POPUP_MARGIN)
|
|
177
|
+
dx = right - (size.width - POPUP_MARGIN);
|
|
178
|
+
if (dx !== 0 || dy !== 0)
|
|
179
|
+
map.panBy({ x: dx, y: dy });
|
|
180
|
+
});
|
|
181
|
+
}, []);
|
|
182
|
+
/*
|
|
183
|
+
* Zdarzenia silnika czytają najświeższe dane przez referencję: silnik montujemy raz,
|
|
184
|
+
* a jego callbacki żyją dłużej niż render, w którym powstały.
|
|
185
|
+
*/
|
|
186
|
+
const latest = useRef({ entries, allowCreate, popup, contextTarget, onLocationClick, onClusterClick, onViewChange });
|
|
187
|
+
latest.current = { entries, allowCreate, popup, contextTarget, onLocationClick, onClusterClick, onViewChange };
|
|
188
|
+
const emitViewChange = useCallback(() => {
|
|
189
|
+
const map = engineRef.current;
|
|
190
|
+
if (!map)
|
|
191
|
+
return;
|
|
192
|
+
setZoom(map.getZoom());
|
|
193
|
+
latest.current.onViewChange?.({ center: map.getCenter(), zoom: map.getZoom(), bounds: map.getBounds() });
|
|
194
|
+
}, []);
|
|
195
|
+
const onMarkerClick = useCallback((id) => {
|
|
196
|
+
setContextTarget(null);
|
|
197
|
+
const entry = latest.current.entries.find((candidate) => candidate.id === id);
|
|
198
|
+
if (!entry)
|
|
199
|
+
return;
|
|
200
|
+
if (isCluster(entry)) {
|
|
201
|
+
latest.current.onClusterClick?.(entry);
|
|
202
|
+
// Grupa punktów o tej samej współrzędnej nie da się rozbić zoomem — pokaż listę.
|
|
203
|
+
if (isSplittable(entry)) {
|
|
204
|
+
engineRef.current?.fitBounds(entry.bounds, FIT_PADDING);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const state = { location: entry.items[0], group: entry };
|
|
208
|
+
setPopup(state);
|
|
209
|
+
autoPanToPopup(state);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const state = { location: entry, group: null };
|
|
213
|
+
setPopup(state);
|
|
214
|
+
autoPanToPopup(state);
|
|
215
|
+
latest.current.onLocationClick?.(entry);
|
|
216
|
+
}, [autoPanToPopup]);
|
|
217
|
+
/*
|
|
218
|
+
* Montaż silnika — raz na życie komponentu.
|
|
219
|
+
*
|
|
220
|
+
* Sprzątanie czeka na zakończenie `init()`: w trybie ścisłym React montuje efekt dwa razy,
|
|
221
|
+
* a silnik mapowy oznacza kontener jako zajęty już przy pierwszym wywołaniu. Zniszczenie
|
|
222
|
+
* „w locie", przed rozwiązaniem obietnicy, zostawiłoby oznaczony kontener i drugi montaż
|
|
223
|
+
* kończyłby się błędem „Map container is already initialized".
|
|
224
|
+
*/
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
let disposed = false;
|
|
227
|
+
const map = engine();
|
|
228
|
+
engineRef.current = map;
|
|
229
|
+
setEngineKind(map.kind);
|
|
230
|
+
const preset = resolveCountryView(country);
|
|
231
|
+
const canvas = canvasRef.current;
|
|
232
|
+
if (!canvas)
|
|
233
|
+
throw new Error(NO_ENGINE_ERROR);
|
|
234
|
+
mountChain.current = mountChain.current
|
|
235
|
+
.then(() => map.init(canvas, {
|
|
236
|
+
center: view?.center ?? preset.center,
|
|
237
|
+
zoom: view?.zoom ?? preset.zoom,
|
|
238
|
+
maxBounds: restrictBounds(),
|
|
239
|
+
tileUrl: tileUrl ?? KPT_MAP_DEFAULT_TILE_URL,
|
|
240
|
+
attribution: attribution ?? KPT_MAP_DEFAULT_ATTRIBUTION,
|
|
241
|
+
}))
|
|
242
|
+
.then(() => {
|
|
243
|
+
if (disposed)
|
|
244
|
+
return;
|
|
245
|
+
// Jawny zasięg wygrywa nad środkiem i zoomem: dopiero teraz znamy rozmiar kontenera.
|
|
246
|
+
if (bounds && !view)
|
|
247
|
+
map.fitBounds(bounds, FIT_PADDING);
|
|
248
|
+
// Silnik emituje `move` co klatkę przeciągania. Bez nakładek nikt tej pozycji nie czyta.
|
|
249
|
+
map.on('move', () => {
|
|
250
|
+
if (latest.current.popup || latest.current.contextTarget)
|
|
251
|
+
setViewportTick((tick) => tick + 1);
|
|
252
|
+
});
|
|
253
|
+
map.on('moveend', () => emitViewChange());
|
|
254
|
+
map.on('zoomend', () => setZoom(map.getZoom()));
|
|
255
|
+
map.on('click', () => {
|
|
256
|
+
setPopup(null);
|
|
257
|
+
setContextTarget(null);
|
|
258
|
+
setSearchPin(null);
|
|
259
|
+
});
|
|
260
|
+
map.on('contextmenu', (event) => {
|
|
261
|
+
if (!latest.current.allowCreate)
|
|
262
|
+
return;
|
|
263
|
+
setPopup(null);
|
|
264
|
+
setContextTarget({ at: event.at, point: event.point });
|
|
265
|
+
});
|
|
266
|
+
map.on('markerclick', (id) => onMarkerClick(id));
|
|
267
|
+
setZoom(map.getZoom());
|
|
268
|
+
setReady(true);
|
|
269
|
+
emitViewChange();
|
|
270
|
+
});
|
|
271
|
+
return () => {
|
|
272
|
+
disposed = true;
|
|
273
|
+
engineRef.current = null;
|
|
274
|
+
setReady(false);
|
|
275
|
+
// Zniszczenie dopinamy do łańcucha: dopiero po `init()`, bo inaczej `destroy()` trafiłby
|
|
276
|
+
// w mapę, której jeszcze nie ma, a kontener zostałby oznaczony jako zajęty na zawsze.
|
|
277
|
+
mountChain.current = mountChain.current.finally(() => map.destroy());
|
|
278
|
+
};
|
|
279
|
+
// Silnik i widok startowy czytamy raz — podmiana po starcie nie przeładowuje mapy.
|
|
280
|
+
}, []);
|
|
281
|
+
// Bez tego Leaflet zostawia szare pola po każdej zmianie układu strony.
|
|
282
|
+
useLayoutEffect(() => {
|
|
283
|
+
const canvas = canvasRef.current;
|
|
284
|
+
if (!canvas || typeof ResizeObserver === 'undefined')
|
|
285
|
+
return;
|
|
286
|
+
const observer = new ResizeObserver(() => engineRef.current?.invalidateSize());
|
|
287
|
+
observer.observe(canvas);
|
|
288
|
+
return () => observer.disconnect();
|
|
289
|
+
}, []);
|
|
290
|
+
// Wypychanie znaczników do silnika — jedyne miejsce, które je rysuje.
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (ready)
|
|
293
|
+
engineRef.current?.setMarkers(markers);
|
|
294
|
+
}, [ready, markers]);
|
|
295
|
+
// Zmiana źródła kafli w locie — bez przeładowania mapy.
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (ready) {
|
|
298
|
+
engineRef.current?.setTileSource(tileUrl ?? KPT_MAP_DEFAULT_TILE_URL, attribution ?? KPT_MAP_DEFAULT_ATTRIBUTION);
|
|
299
|
+
}
|
|
300
|
+
}, [ready, tileUrl, attribution]);
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (ready)
|
|
303
|
+
engineRef.current?.setMaxBounds(restrictBounds());
|
|
304
|
+
}, [ready, restrictBounds]);
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!ready)
|
|
307
|
+
return;
|
|
308
|
+
engineRef.current?.setUserPosition(userPosition, renderUserMarkerHtml({ label: t('map.myLocation'), iconSvgInner: getKptIcon('user') }));
|
|
309
|
+
}, [ready, userPosition, t]);
|
|
310
|
+
// Śledzenie pozycji użytkownika. Odmowa zgody wyłącza kontrolkę, ale nie wywraca komponentu.
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!showUserLocation || !geolocation) {
|
|
313
|
+
setUserPosition(null);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
setLocationBlocked(false);
|
|
317
|
+
return geolocation.watch((position) => {
|
|
318
|
+
setUserPosition(position);
|
|
319
|
+
onUserPositionChange?.(position);
|
|
320
|
+
if (position && followUser) {
|
|
321
|
+
const map = engineRef.current;
|
|
322
|
+
if (map)
|
|
323
|
+
map.setView(position.at, Math.max(map.getZoom(), 13));
|
|
324
|
+
}
|
|
325
|
+
}, (error) => {
|
|
326
|
+
setUserPosition(null);
|
|
327
|
+
setLocationBlocked(true);
|
|
328
|
+
onUserLocationError?.(error);
|
|
329
|
+
});
|
|
330
|
+
// `followUser` czytamy w callbacku, więc nie restartuje nasłuchu.
|
|
331
|
+
}, [showUserLocation, geolocation]);
|
|
332
|
+
const popupCategory = popup ? categoryById.get(popup.location.categoryId) : undefined;
|
|
333
|
+
/**
|
|
334
|
+
* Odległość otwartego punktu od `distanceFrom` (albo od użytkownika) w metrach. `null`, gdy
|
|
335
|
+
* nie ma od czego liczyć — treść popupu chowa wtedy wiersz dystansu zamiast pokazywać zero.
|
|
336
|
+
*/
|
|
337
|
+
const popupDistance = useMemo(() => {
|
|
338
|
+
if (!popup)
|
|
339
|
+
return null;
|
|
340
|
+
const origin = distanceFrom ?? userPosition?.at ?? null;
|
|
341
|
+
return origin ? haversine(origin, popup.location.at) : null;
|
|
342
|
+
}, [popup, distanceFrom, userPosition]);
|
|
343
|
+
/** Pozycje nakładek w pikselach kontenera; zależą od `viewportTick`, czyli od ruchu mapy. */
|
|
344
|
+
const popupPosition = useMemo(() => {
|
|
345
|
+
void viewportTick;
|
|
346
|
+
if (!popup || !engineRef.current || !ready)
|
|
347
|
+
return null;
|
|
348
|
+
return engineRef.current.project(popup.location.at);
|
|
349
|
+
}, [viewportTick, popup, ready]);
|
|
350
|
+
const contextPosition = useMemo(() => {
|
|
351
|
+
void viewportTick;
|
|
352
|
+
if (!contextTarget || !engineRef.current || !ready)
|
|
353
|
+
return null;
|
|
354
|
+
return engineRef.current.project(contextTarget.at);
|
|
355
|
+
}, [viewportTick, contextTarget, ready]);
|
|
356
|
+
/** Kategorie oferowane w menu „dodaj" — ukryte w legendzie odpadają. */
|
|
357
|
+
const creatableCategories = useMemo(() => {
|
|
358
|
+
const hiddenSet = new Set(hidden);
|
|
359
|
+
return categories.filter((category) => !hiddenSet.has(category.id) && (category.fields?.length || renderCreateForm));
|
|
360
|
+
}, [categories, hidden, renderCreateForm]);
|
|
361
|
+
/** Czy jest czym szukać — port bez `search()` nie zasługuje na pole, które nic nie robi. */
|
|
362
|
+
const searchEnabled = search && !!geocoder?.search;
|
|
363
|
+
/**
|
|
364
|
+
* Prostokąt preferowanych wyników: zasięg jawny, a bez niego preset kraju — „Kwiatowa" ma
|
|
365
|
+
* trafiać w tę pod nosem. To sam bias: adresu z zagranicy dalej da się poszukać.
|
|
366
|
+
*/
|
|
367
|
+
const searchViewbox = bounds ?? resolveCountryView(country).bounds;
|
|
368
|
+
/**
|
|
369
|
+
* Twarde zawężenie do krajów. Jawne `searchOptions.countryCodes` wygrywa; bez niego odcinamy
|
|
370
|
+
* zagranicę dopiero przy `restrictToCountry` — tam mapa i tak nie pozwoli tam przesunąć widoku.
|
|
371
|
+
*/
|
|
372
|
+
const searchCountryCodes = searchOptions?.countryCodes ?? (restrictToCountry ? [country] : null);
|
|
373
|
+
const closeCreate = () => {
|
|
374
|
+
setCreateOpen(false);
|
|
375
|
+
setCreateFor(null);
|
|
376
|
+
};
|
|
377
|
+
const emitCreate = (value) => {
|
|
378
|
+
if (!createFor)
|
|
379
|
+
return;
|
|
380
|
+
onLocationCreate?.({ at: createFor.at, categoryId: createFor.category.id, value });
|
|
381
|
+
closeCreate();
|
|
382
|
+
};
|
|
383
|
+
const zoomBy = (delta) => {
|
|
384
|
+
const map = engineRef.current;
|
|
385
|
+
if (map)
|
|
386
|
+
map.setView(map.getCenter(), map.getZoom() + delta);
|
|
387
|
+
};
|
|
388
|
+
const centreOnUser = () => {
|
|
389
|
+
const map = engineRef.current;
|
|
390
|
+
if (!userPosition || !map)
|
|
391
|
+
return;
|
|
392
|
+
map.setView(userPosition.at, Math.max(map.getZoom(), 13));
|
|
393
|
+
};
|
|
394
|
+
const handleSearchSelect = (result) => {
|
|
395
|
+
setPopup(null);
|
|
396
|
+
setContextTarget(null);
|
|
397
|
+
setSearchPin(result);
|
|
398
|
+
const map = engineRef.current;
|
|
399
|
+
// Zasięg (miasto, region) wpasowujemy w kadr, a pojedynczy punkt dostaje `searchZoom`.
|
|
400
|
+
if (result.bounds)
|
|
401
|
+
map?.fitBounds(result.bounds, FIT_PADDING);
|
|
402
|
+
else
|
|
403
|
+
map?.setView(result.at, searchZoom);
|
|
404
|
+
onSearchSelect?.(result);
|
|
405
|
+
};
|
|
406
|
+
return (_jsxs("div", { className: cn('kpt-map', className), "data-engine": engineKind ?? undefined, style: { ...style, '--kpt-map-height': height }, ...rest, children: [_jsxs("div", { className: "kpt-map__viewport", children: [_jsx("div", { ref: canvasRef, className: "kpt-map__canvas" }), _jsxs("div", { className: "kpt-map__controls", role: "group", "aria-label": t('map.legend'), children: [_jsx("button", { className: "kpt-map__control", type: "button", "aria-label": t('map.zoomIn'), title: t('map.zoomIn'), disabled: !ready, onClick: () => zoomBy(1), children: _jsx("span", { "aria-hidden": "true", children: "+" }) }), _jsx("button", { className: "kpt-map__control", type: "button", "aria-label": t('map.zoomOut'), title: t('map.zoomOut'), disabled: !ready, onClick: () => zoomBy(-1), children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) }), showUserLocation ? (_jsx("button", { className: "kpt-map__control kpt-map__control--locate", type: "button", "aria-label": locationBlocked ? t('map.locationDenied') : t('map.locateMe'), title: locationBlocked ? t('map.locationDenied') : t('map.locateMe'), disabled: !userPosition, onClick: centreOnUser, children: _jsx("span", { className: "kpt-map__crosshair", "aria-hidden": "true" }) })) : null] }), searchEnabled ? (_jsx("div", { className: "kpt-map__search", children: _jsx(KptMapSearch, { geocoder: geocoder, placeholder: searchPlaceholder, minLength: searchOptions?.minLength ?? 3, debounce: searchOptions?.debounce ?? 300, limit: searchOptions?.limit ?? 5, viewbox: searchViewbox, countryCodes: searchCountryCodes, onSelect: handleSearchSelect, onClear: () => setSearchPin(null) }) })) : null, allowCreate && contextTarget && contextPosition ? (_jsxs("div", { className: "kpt-map__context", role: "menu", "aria-label": t('map.addLocation'), style: { '--_kpt-map-x': `${contextPosition.x}px`, '--_kpt-map-y': `${contextPosition.y}px` }, children: [_jsx("p", { className: "kpt-map__context-title", children: t('map.chooseCategory') }), creatableCategories.length === 0 ? (_jsx("p", { className: "kpt-map__context-empty", children: t('map.empty') })) : (creatableCategories.map((category) => (_jsxs("button", { className: "kpt-map__context-item", type: "button", role: "menuitem", onClick: () => {
|
|
407
|
+
setContextTarget(null);
|
|
408
|
+
setCreateFor({ at: contextTarget.at, category });
|
|
409
|
+
setCreateOpen(true);
|
|
410
|
+
}, children: [_jsx("span", { className: "kpt-map__context-dot", "aria-hidden": "true" }), category.label] }, category.id))))] })) : null, popup && popupPosition ? (_jsxs("div", { ref: popupRef, className: "kpt-map__popup", role: "dialog", "data-layout": popupLayout, "aria-label": popup.location.title, style: { '--_kpt-map-x': `${popupPosition.x}px`, '--_kpt-map-y': `${popupPosition.y}px` }, children: [popupLayout === 'plain' ? (_jsx("button", { className: "kpt-map__popup-close", type: "button", "aria-label": t('common.close'), onClick: () => setPopup(null), children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })) : null, popup.group ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "kpt-map__popup-group", children: t('map.clusterItems') }), _jsx("div", { className: "kpt-map__popup-tabs", role: "tablist", children: popup.group.items.map((item) => (_jsx("button", { className: "kpt-map__popup-tab", type: "button", role: "tab", "aria-selected": item.id === popup.location.id, onClick: () => {
|
|
411
|
+
setPopup({ location: item, group: popup.group });
|
|
412
|
+
onLocationClick?.(item);
|
|
413
|
+
}, children: item.title }, item.id))) })] })) : null, _jsx("div", { className: "kpt-map__popup-body", children: renderPopup ? (renderPopup({
|
|
414
|
+
location: popup.location,
|
|
415
|
+
data: popup.location.data,
|
|
416
|
+
category: popupCategory,
|
|
417
|
+
distance: popupDistance,
|
|
418
|
+
close: () => setPopup(null),
|
|
419
|
+
})) : (_jsxs(_Fragment, { children: [_jsx("strong", { className: "kpt-map__popup-title", children: popup.location.title }), popupCategory ? _jsx("p", { className: "kpt-map__popup-category", children: popupCategory.label }) : null] })) })] })) : null] }), legend ? (_jsx(KptMapLegend, { className: "kpt-map__legend", categories: categories, hiddenCategories: hidden, onHiddenCategoriesChange: setHidden, counts: counts })) : null, _jsx(KptDialog, { open: createOpen, onOpenChange: (next) => (next ? setCreateOpen(true) : closeCreate()), header: t('map.addLocation'), children: createFor ? (renderCreateForm ? (renderCreateForm({
|
|
420
|
+
at: createFor.at,
|
|
421
|
+
category: createFor.category,
|
|
422
|
+
close: closeCreate,
|
|
423
|
+
submit: emitCreate,
|
|
424
|
+
})) : (_jsx(KptMapCreateDialog, { geocoder: geocoder, category: createFor.category, at: createFor.at, onSave: emitCreate, onCancel: closeCreate }))) : null })] }));
|
|
425
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { KptLatLng, KptMapAddress, KptMapBounds, KptMapGeolocationError, KptMapSearchResult, KptMapSuggestion, KptMapUserPosition } from '@konce-pt/map';
|
|
2
|
+
export interface KptMapSearchOptions {
|
|
3
|
+
/** Język odpowiedzi. Domyślnie to, co ustawi dostawca. */
|
|
4
|
+
language?: string;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
/** Ile wyników najwyżej. Domyślnie 5 — to lista podpowiedzi, nie wyszukiwarka pełnotekstowa. */
|
|
7
|
+
limit?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Preferuj wyniki z tego prostokąta. **Nie odcina** reszty świata — adres z zagranicy
|
|
10
|
+
* dalej da się znaleźć, tylko wypada niżej na liście.
|
|
11
|
+
*/
|
|
12
|
+
viewbox?: KptMapBounds;
|
|
13
|
+
/** Twarde zawężenie do krajów (ISO 3166-1 alpha-2). Wyniki spoza nich nie wrócą wcale. */
|
|
14
|
+
countryCodes?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
export interface KptMapGeocoderPort {
|
|
17
|
+
/** `null` = nie udało się ustalić adresu. */
|
|
18
|
+
reverse(at: KptLatLng, options?: {
|
|
19
|
+
language?: string;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}): Promise<KptMapAddress | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Geokodowanie wprost: tekst → miejsca. Metoda jest **opcjonalna** — port bez niej po prostu
|
|
24
|
+
* nie dostaje pola wyszukiwania (komponent go wtedy nie renderuje).
|
|
25
|
+
*/
|
|
26
|
+
search?(query: string, options?: KptMapSearchOptions): Promise<readonly KptMapSuggestion[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Uzupełnia podpowiedź o współrzędne. Wołane **raz, po wybraniu pozycji z listy** — nigdy
|
|
29
|
+
* w trakcie pisania. Potrzebne tylko dostawcom, których `search()` nie zwraca `at`.
|
|
30
|
+
*/
|
|
31
|
+
resolve?(suggestion: KptMapSuggestion, options?: {
|
|
32
|
+
language?: string;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}): Promise<KptMapSearchResult | null>;
|
|
35
|
+
}
|
|
36
|
+
export interface KptMapGeolocationPort {
|
|
37
|
+
/**
|
|
38
|
+
* Śledzi pozycję użytkownika. Zwraca funkcję sprzątającą — tak samo jak efekt Reacta,
|
|
39
|
+
* więc wpina się bez adaptera.
|
|
40
|
+
*/
|
|
41
|
+
watch(onPosition: (position: KptMapUserPosition | null) => void, onError: (error: KptMapGeolocationError) => void): () => void;
|
|
42
|
+
}
|
|
43
|
+
/** Geolokalizacja przeglądarki. Brak zgody czy brak API nie wywraca komponentu. */
|
|
44
|
+
export declare function browserGeolocation(options?: PositionOptions): KptMapGeolocationPort;
|
|
45
|
+
/** Stała pozycja — do dem, testów i SSR, gdzie prawdziwej geolokalizacji nie ma. */
|
|
46
|
+
export declare function geolocationStub(at: KptLatLng, accuracy?: number): KptMapGeolocationPort;
|
|
47
|
+
export interface KptNominatimOptions {
|
|
48
|
+
/** Adres instancji Nominatim. Domyślnie publiczna instancja OSM — patrz ostrzeżenie niżej. */
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
/** Język odpowiedzi (`Accept-Language`). Domyślnie język przeglądarki. */
|
|
51
|
+
language?: string;
|
|
52
|
+
/** Szczegółowość: 18 to poziom budynku, 16 ulicy, 10 miasta. Domyślnie 18. */
|
|
53
|
+
zoom?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Najmniejszy odstęp między zapytaniami w milisekundach.
|
|
56
|
+
*
|
|
57
|
+
* Sekunda nie jest cechą Nominatima, tylko wymogiem regulaminu **publicznej instancji OSM** —
|
|
58
|
+
* dlatego domyślna zależy od `baseUrl`: bez niego 1000 ms, z własnym adresem 0.
|
|
59
|
+
*/
|
|
60
|
+
minIntervalMs?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Adapter Nominatim (OpenStreetMap): geokodowanie wsteczne i podpowiedzi adresowe.
|
|
64
|
+
*
|
|
65
|
+
* Kolejkuje zapytania z odstępem sekundy, bo tego wymaga regulamin publicznej instancji.
|
|
66
|
+
* Odstęp liczony od zegara, a nie stałym opóźnieniem przed każdym zapytaniem — inaczej
|
|
67
|
+
* pierwsza podpowiedź po minucie bezczynności też płaciłaby sekundę.
|
|
68
|
+
*
|
|
69
|
+
* **Nie rejestruj publicznej instancji w produkcji bez przeczytania jej polityki użycia.**
|
|
70
|
+
*/
|
|
71
|
+
export declare function createNominatimGeocoder(options?: KptNominatimOptions): KptMapGeocoderPort;
|
|
72
|
+
/** Port bez sieci: stała lista podpowiedzi i stały adres. Do dem i testów. */
|
|
73
|
+
export declare function createGeocoderStub(options?: {
|
|
74
|
+
address?: KptMapAddress | null;
|
|
75
|
+
suggestions?: readonly KptMapSuggestion[];
|
|
76
|
+
}): KptMapGeocoderPort;
|