Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
W tym szybkim starcie utworzysz aplikację internetową, która wyświetla obrazy satelitarne i dane geoprzestrzenne z twojego GeoCatalogu na interaktywnej mapie. Uwierzytelniasz użytkowników za pomocą identyfikatora Entra firmy Microsoft, wykonujesz zapytania dotyczące kolekcji STAC i renderujesz kafelki mapy — wszystko z poziomu przeglądarki JavaScript.
Czego się nauczysz:
- Uwierzytelnianie użytkowników i uzyskiwanie tokenów dostępu przy użyciu MSAL.js
- Wykonywanie zapytań względem interfejsu API STAC w celu odnajdywania kolekcji i elementów
- Wyświetlanie kafelków rastrowych na mapie MapLibre GL z użyciem nagłówków autoryzacji
- Tworzenie bezproblemowych warstw mozaiki w całej kolekcji
- Pobieranie nieprzetworzonych zasobów przy użyciu tokenów SAS
Wzorce kodu działają z dowolnym nowoczesnym frameworkiem JavaScript (React, Vue, Angular) lub w czystym JavaScripcie. Interfejsy API GeoCatalog mają pełną obsługę mechanizmu CORS, dzięki czemu można je wywoływać bezpośrednio z localhost podczas rozwoju — bez wymaganego serwera proxy.
Możesz pobrać i przetestować ten kod z publicznego repozytorium GitHub Microsoft Planetary Computer Pro.
Wymagania wstępne
- Konto Azure z aktywną subskrypcją. Utwórz konto bezpłatnie.
- Wdrożony zasób GeoCatalog z co najmniej jedną kolekcją zawierającą elementy.
- Tożsamość użytkownika musi mieć dostęp Czytelnika GeoCatalogu (lub wyższego poziomu) do zasobu GeoCatalog. Zobacz Zarządzanie dostępem do zasobu GeoCatalog.
- Node.js w wersji 18 lub nowszej.
Przegląd architektury
Typowa aplikacja internetowa GeoCatalog jest zgodna z tą architekturą:
Rejestrowanie aplikacji w identyfikatorze Entra firmy Microsoft
Zanim aplikacja internetowa będzie mogła uwierzytelnić użytkowników, zarejestruj ją w identyfikatorze Entra firmy Microsoft. W tym przewodniku Szybki start jest używana rejestracja aplikacji jednostronicowej (SPA), która jest idealna dla aplikacji JavaScript po stronie klienta i programowania lokalnego. Wzorce integracji interfejsu API pokazane w kolejnych krokach działają z dowolnym typem aplikacji.
Uwaga / Notatka
W przypadku aplikacji produkcyjnych z serwerem backend rozważ wybranie innego typu rejestracji (Web, Natywna, itp.). Zobacz Konfigurowanie uwierzytelniania aplikacji , aby uzyskać wskazówki dotyczące wybierania odpowiedniego podejścia dla danego scenariusza.
Rejestrowanie jako jednostronicowa aplikacja
- Przejdź do elementu Microsoft Entra ID w portalu Azure.
- Wybierz pozycję Rejestracje aplikacji w panelu bocznym.
- Wybierz opcję Nowa rejestracja.
- Wprowadź nazwę aplikacji (na przykład "GeoCatalog Web App").
- Pod Obsługiwane typy kont wybierz Konta tylko w tym katalogu organizacyjnym.
- W obszarze Identyfikator URI przekierowania wybierz pozycję Aplikacja jednostronicowa (SPA) i wprowadź adres URL deweloperski (na przykład
http://localhost:5173). - Wybierz pozycję Zarejestruj.
Po rejestracji zanotuj następujące wartości na stronie Przegląd :
- Identyfikator aplikacji (klienta)
- Identyfikator katalogu (klienta)
Aby uzyskać więcej informacji, zapoznaj się z artykułem Rejestracja aplikacji szybki start.
Przyznawanie uprawnień API
Aplikacja musi mieć uprawnienia do wywoływania interfejsu API GeoCatalog w imieniu zalogowanych użytkowników.
- W rejestracji aplikacji wybierz pozycję Uprawnienia interfejsu API>Dodaj uprawnienie.
- Wybierz Interfejsy API używane przez moją organizację i wyszukaj Azure Orbital Spatio.
- Wybierz Delegowane uprawnienia i zaznacz user_impersonation.
- Wybierz Dodaj uprawnienia.
- Jeśli jesteś administratorem, wybierz pozycję Udziel zgody administratora na wyrażenie zgody w imieniu wszystkich użytkowników w dzierżawie.
Konfigurowanie aplikacji
Aplikacja wymaga następujących wartości konfiguracji. Sposób podawania tych wartości zależy od narzędzi kompilacji (zmiennych środowiskowych, plików konfiguracji itd.):
| Konfiguracja | Wartość | Description |
|---|---|---|
| Adres URL wykazu | https://{name}.{region}.geocatalog.spatio.azure.com |
Punkt końcowy GeoKatalogu |
| Identyfikator dzierżawy | Z rejestracji aplikacji | Twoja dzierżawa Microsoft Entra |
| ID klienta | Z rejestracji aplikacji | Identyfikator klienta aplikacji |
| Zakres interfejsu API | https://geocatalog.spatio.azure.com/.default |
Zawsze używaj tej dokładnej wartości |
Instalowanie zależności
Zainstaluj bibliotekę Microsoft Authentication Library (MSAL) dla aplikacji przeglądarki i bibliotekę mapy:
npm install @azure/msal-browser maplibre-gl
- @azure/msal-browser — obsługuje uwierzytelnianie OAuth 2.0 przy użyciu identyfikatora Entra firmy Microsoft
- maplibre-gl — biblioteka map typu open source na potrzeby wizualizacji kafelków
Wskazówka
Struktura projektu: Przykłady kodu w tym przewodniku szybkiego startu to funkcje autonomiczne, które można organizować według własnych preferencji. Typowy wzorzec:
-
auth.js: Funkcje konfiguracji MSAL oraz tokenów -
api.js: funkcje interfejsu API STAC, interfejsu API kafelkowania i tokenu SAS -
map.js: Inicjowanie mapLibre i zarządzanie warstwami kafelków -
App.jslubmain.js: Połącz wszystko razem z interfejsem użytkownika
Każda funkcja odbiera zależności (tokeny dostępu, adresy URL) jako parametry, co ułatwia ich integrację z dowolną strukturą lub strukturą projektu.
Implementowanie uwierzytelniania MSAL
Skonfiguruj bibliotekę MSAL na potrzeby uwierzytelniania przeglądarki. W poniższym przykładzie przedstawiono konfigurację kluczy i wzorzec pozyskiwania tokenu.
import { PublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser';
// Configuration - replace with your values or load from environment/config
const msalConfig = {
auth: {
clientId: 'YOUR_CLIENT_ID',
authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID',
redirectUri: window.location.origin,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
};
// Create MSAL instance
const msalInstance = new PublicClientApplication(msalConfig);
// GeoCatalog API scope - always use this exact value
const scopes = ['https://geocatalog.spatio.azure.com/.default'];
/**
* Acquire an access token for GeoCatalog API calls.
* Tries silent acquisition first, falls back to popup if needed.
*/
async function getAccessToken() {
const account = msalInstance.getActiveAccount() || msalInstance.getAllAccounts()[0];
if (!account) {
throw new Error('No authenticated account. Call login() first.');
}
try {
// Try silent token acquisition (uses cached token)
const result = await msalInstance.acquireTokenSilent({ account, scopes });
return result.accessToken;
} catch (error) {
// If silent fails (token expired), fall back to popup
if (error instanceof InteractionRequiredAuthError) {
const result = await msalInstance.acquireTokenPopup({ scopes });
return result.accessToken;
}
throw error;
}
}
/**
* Sign in the user via popup.
*/
async function login() {
const result = await msalInstance.loginPopup({ scopes });
msalInstance.setActiveAccount(result.account);
return result.account;
}
/**
* Sign out the user.
*/
function logout() {
msalInstance.logoutPopup();
}
Interfejs API STAC: kolekcje zapytań i elementy
Interfejs API STAC geocatalog udostępnia punkty końcowe do odnajdywania i wykonywania zapytań dotyczących danych geoprzestrzennych. Wszystkie żądania wymagają nagłówka Authorization z tokenem elementu nośnego uzyskanego z uwierzytelniania MSAL.
Listy kolekcji
const API_VERSION = '2025-04-30-preview';
async function listCollections(accessToken, catalogUrl) {
const url = `${catalogUrl}/stac/collections?api-version=${API_VERSION}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to list collections: ${response.statusText}`);
}
const data = await response.json();
return data.collections; // Array of STAC Collection objects
}
Wyświetlanie listy elementów w kolekcji
const API_VERSION = '2025-04-30-preview';
async function listItems(accessToken, catalogUrl, collectionId, limit = 10) {
const url = `${catalogUrl}/stac/collections/${collectionId}/items?limit=${limit}&api-version=${API_VERSION}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to list items: ${response.statusText}`);
}
const data = await response.json();
return data.features; // Array of STAC Item objects
}
Wyszukiwanie w różnych kolekcjach
const API_VERSION = '2025-04-30-preview';
async function searchItems(accessToken, catalogUrl, searchParams) {
const url = `${catalogUrl}/stac/search?api-version=${API_VERSION}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(searchParams),
});
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
return await response.json();
}
// Example usage:
const results = await searchItems(token, catalogUrl, {
collections: ['my-collection'],
bbox: [-122.5, 37.5, -122.0, 38.0], // [west, south, east, north]
datetime: '2024-01-01/2024-12-31',
limit: 20,
});
Adresy URL kafelków: Tworzenie adresów URL na potrzeby wizualizacji mapy
Interfejs API GeoCatalog Tiler służy do serwowania danych rastrowych jako kafelki mapy. Skonstruuj adresy URL kafelków przy użyciu następującego wzorca:
Kafelki pojedyncze
{catalogUrl}/data/collections/{collectionId}/items/{itemId}/tiles/{z}/{x}/{y}@1x.png
?api-version=2025-04-30-preview
&tileMatrixSetId=WebMercatorQuad
&assets=visual
Funkcja konstruktora adresów URL kafelka
const API_VERSION = '2025-04-30-preview';
/**
* Build a tile URL template for a STAC item.
* Returns a URL with {z}/{x}/{y} placeholders for use with map libraries.
*/
function buildTileUrl(catalogUrl, collectionId, itemId, options = {}) {
const { assets = 'visual', colormap, rescale } = options;
const base = `${catalogUrl}/data/collections/${collectionId}/items/${itemId}/tiles/{z}/{x}/{y}@1x.png`;
const params = new URLSearchParams();
params.set('api-version', API_VERSION);
params.set('tileMatrixSetId', 'WebMercatorQuad');
params.set('assets', assets);
if (colormap) params.set('colormap_name', colormap);
if (rescale) params.set('rescale', rescale);
return `${base}?${params.toString()}`;
}
// Example usage:
const tileUrl = buildTileUrl(
'https://mygeocatalog.northcentralus.geocatalog.spatio.azure.com',
'aerial-imagery',
'image-001',
{ assets: 'visual' }
);
Kluczowe parametry kafelka
| Parameter | Wymagane | Description |
|---|---|---|
api-version |
Tak | Wersja interfejsu API (2025-04-30-preview) |
tileMatrixSetId |
Tak | Używanie WebMercatorQuad do map internetowych |
assets |
Tak | Nazwy elementów zawartości do renderowania (na przykład: visual, image) |
colormap_name |
Nie. | Nazwana mapa kolorów (przykład: viridis, terrain) |
rescale |
Nie. | Zakres wartości do skalowania (na przykład: 0,255) |
asset_bidx |
Nie. | Indeksy pasmowe (na przykład: image\|1,2,3 dla RGB) |
Uwaga / Notatka
W przypadku kolekcji z obrazami czteropasmowymi (na przykład NAIP z RGB + NIR) użyj asset_bidx=image|1,2,3 polecenia, aby wybrać tylko pasma RGB. Moduł układający nie może zakodować czterech pasm w formacie PNG.
Integracja mapy: wyświetlanie kafelków za pomocą biblioteki MapLibre GL
Biblioteki map, takie jak MapLibre GL, Ulotka i OpenLayers, mogą wyświetlać kafelki rastrowe. Kluczowym wyzwaniem jest dodanie nagłówków autoryzacyjnych do żądań dotyczących kafelków, ponieważ biblioteki te pobierają kafelki bezpośrednio.
Przykład mapLibre GL
MapLibre GL udostępnia transformRequest opcję dodawania nagłówków:
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
// Store the current access token
let currentAccessToken = null;
function initializeMap(containerId, accessToken) {
currentAccessToken = accessToken;
const map = new maplibregl.Map({
container: containerId,
style: {
version: 8,
sources: {
osm: {
type: 'raster',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors',
},
},
layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
},
center: [0, 0],
zoom: 2,
// Add authorization header to tile requests
transformRequest: (url, resourceType) => {
// Only add auth for GeoCatalog tile requests
if (url.includes('geocatalog.spatio.azure.com') && currentAccessToken) {
return {
url,
headers: { 'Authorization': `Bearer ${currentAccessToken}` },
};
}
return { url };
},
});
return map;
}
function addTileLayer(map, tileUrl, bounds) {
// Remove existing layer and source if present
if (map.getLayer('data-layer')) {
map.removeLayer('data-layer');
}
if (map.getSource('data-tiles')) {
map.removeSource('data-tiles');
}
// Add tile source
map.addSource('data-tiles', {
type: 'raster',
tiles: [tileUrl],
tileSize: 256,
minzoom: 10, // Many aerial collections require zoom 10+
maxzoom: 18,
});
// Add tile layer
map.addLayer({
id: 'data-layer',
type: 'raster',
source: 'data-tiles',
});
// Zoom to bounds [west, south, east, north]
if (bounds) {
map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 50 });
}
}
Ważne
Funkcja transformRequest jest wywoływana dla każdego żądania kafelka. Zapisz token dostępu w zmiennej, do której transformRequest ma dostęp, i zaktualizuj go, gdy token zostanie odświeżony.
Kafelki mozaikowe: wyświetlanie obrazów w całej kolekcji
Aby wyświetlić wszystkie elementy w kolekcji jako jednolitą warstwę, skonfiguruj wyszukiwanie mozaiki i użyj zwróconego identyfikatora wyszukiwania:
const API_VERSION = '2025-04-30-preview';
/**
* Register a mosaic search for a collection.
* Returns a search ID that can be used to fetch mosaic tiles.
*/
async function registerMosaic(catalogUrl, collectionId, accessToken) {
const url = `${catalogUrl}/data/mosaic/register?api-version=${API_VERSION}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
collections: [collectionId],
}),
});
if (!response.ok) {
throw new Error(`Failed to register mosaic: ${response.statusText}`);
}
const data = await response.json();
// Note: API returns 'searchid' (lowercase), not 'searchId'
return data.searchid;
}
/**
* Build a mosaic tile URL template.
*/
function buildMosaicTileUrl(catalogUrl, searchId, collectionId, options = {}) {
const { assets = 'visual' } = options;
const base = `${catalogUrl}/data/mosaic/${searchId}/tiles/{z}/{x}/{y}@1x.png`;
const params = new URLSearchParams();
params.set('api-version', API_VERSION);
params.set('tileMatrixSetId', 'WebMercatorQuad');
params.set('collection', collectionId);
params.set('assets', assets);
return `${base}?${params.toString()}`;
}
Tokeny SAS: pobieranie surowych zasobów
Interfejs API SAS udostępnia tokeny ograniczone czasowo do pobierania surowych plików zasobów (GeoTIFF, COG i inne pliki) bezpośrednio z usługi Azure Blob Storage. Użyj tej opcji, jeśli potrzebujesz oryginalnych plików źródłowych, a nie renderowanych kafelków.
Ważne
Tokeny SAS umożliwiają pobieranie tylko w aplikacjach przeglądarki. Ze względu na zasady CORS usługi Azure Blob Storage przeglądarki nie mogą odczytywać danych obiektów blob za pomocą języka JavaScript fetch(). Zobacz sekcję Ograniczenia przeglądarki .
Token podpisu współdzielonego dostępu
const API_VERSION = '2025-04-30-preview';
/**
* Get a SAS token for accessing assets in a collection.
* Returns a token string that can be appended to asset URLs.
*/
async function getCollectionSasToken(accessToken, catalogUrl, collectionId) {
const url = `${catalogUrl}/sas/token/${collectionId}?api-version=${API_VERSION}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to get SAS token: ${response.statusText}`);
}
const data = await response.json();
return data.token; // SAS token string
}
Tworzenie podpisanego adresu URL pobierania
/**
* Build a signed URL for downloading an asset.
* Appends the SAS token to the asset's href.
*/
function buildSignedAssetUrl(assetHref, sasToken) {
const separator = assetHref.includes('?') ? '&' : '?';
return `${assetHref}${separator}${sasToken}`;
}
// Example usage:
const sasToken = await getCollectionSasToken(accessToken, catalogUrl, 'my-collection');
const assetHref = item.assets['visual'].href;
const signedUrl = buildSignedAssetUrl(assetHref, sasToken);
Wyzwalanie pobierania pliku
/**
* Trigger a browser download for an asset file.
* Works by creating a temporary anchor element.
*/
function downloadAsset(signedUrl, filename) {
const link = document.createElement('a');
link.href = signedUrl;
link.download = filename || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Example: Download an asset
downloadAsset(signedUrl, 'aerial-image.tif');
Ograniczenia przeglądarki
Tokeny SAS działają inaczej w przeglądarkach w porównaniu z kodem po stronie serwera:
| Przypadek użycia | Browser | Server-side |
|---|---|---|
| Pobieranie plików (użytkownik wybiera link) | ✅ Działa | ✅ Działa |
Odczytywanie danych obiektów blob za pośrednictwem fetch() |
❌ Zablokowany mechanizm CORS | ✅ Działa |
| Przetwarzanie nieprzetworzonych pikseli w języku JavaScript | ❌ Nie można | ✅ Działa |
Pobieranie w przeglądarce działa, ponieważ nawigacja (kliknięcie linków) pomija CORS. Jednakże żądania do usługi Azure Blob Storage są blokowane, fetch() ponieważ konto magazynu nie uwzględnia pochodzenia twojej aplikacji w swojej polityce CORS.
Jeśli aplikacja musi odczytywać i przetwarzać nieprzetworzone dane zasobów w przeglądarce, zaimplementuj serwer proxy po stronie serwera:
Uwaga / Notatka
Poniższy kod jest uproszczonym przykładem ilustrowania wzorca serwera proxy. W przypadku aplikacji produkcyjnych punkt końcowy serwera proxy powinien uwierzytelniać żądania (na przykład przekazując token Bearer użytkownika lub używając uwierzytelniania sesji) i sprawdzać, czy użytkownik ma autoryzację dostępu do żądanych zasobów.
// ❌ Browser: This fails due to CORS
const response = await fetch(signedUrl);
const data = await response.arrayBuffer(); // Error!
// ✅ Browser: Call your backend instead
const response = await fetch('/api/proxy-asset', {
method: 'POST',
body: JSON.stringify({ collectionId, itemId, assetName })
});
const data = await response.json(); // Works!
Backend może pobrać blob przy użyciu tokenu SAS i zwrócić przetworzone wyniki.
Zagadnienia dotyczące programowania
Pomoc techniczna CORS
Interfejsy API geocatalog obejmują pełną obsługę mechanizmu CORS za pomocą polecenia Access-Control-Allow-Origin: *. Aplikacje oparte na przeglądarce mogą wysyłać bezpośrednie żądania do usługi GeoCatalog z dowolnego źródła, w tym http://localhost podczas opracowywania. Nie jest wymagany żaden serwer proxy ani obejście problemu.
Interfejs API umożliwia Authorization nagłówek w żądaniach CORS, więc uwierzytelnione fetch() wywołania działają bezpośrednio z poziomu przeglądarki JavaScript.
Wybieranie odpowiedniej metody dostępu do danych
| Metoda | Przeglądarka fetch() |
Pobieranie przeglądarki | Server-side | Najlepsze dla |
|---|---|---|---|---|
| API Tiler | ✅ W pełni obsługiwane | ✅ Tak | ✅ Tak | Wizualizacja mapy |
| Tokeny sygnatury dostępu współdzielonego | ❌ Zablokowany mechanizm CORS | ✅ Tak | ✅ Tak | Nieprzetworzone pliki do pobrania |
Tiler API: używany do wyświetlania obrazu na mapach. Zwraca renderowane kafelki PNG lub WebP z pełną obsługą mechanizmu CORS. Zobacz Adresy URL kafelków i Integracja Map.
Tokeny SAS: służą do pobierania oryginalnych plików źródłowych (GeoTIFF, COG). Pobieranie przeglądarki działa, ale
fetch()jest blokowane przez zasady CORS usługi Azure Blob Storage. Aby uzyskać szczegółowe informacje i obejścia, zobacz Tokeny SAS .
Odświeżanie tokenu
Tokeny dostępu wygasają zazwyczaj po godzinie. Aplikacja powinna:
- Zarządzaj błędami 401 poprzez uzyskanie nowego tokenu.
- Użyj pozyskiwania tokenu w trybie dyskretnym biblioteki MSAL, która automatycznie odświeża wygasłe tokeny.
- Zaktualizuj odwołanie do tokenu używane przez mapę
transformRequest.
Obsługa błędów
Obsługa typowych scenariuszy błędów:
| Status HTTP | Przyczyna | Rozwiązanie |
|---|---|---|
| 401 | Token wygasł lub jest nieprawidłowy | Odświeżanie tokenu dostępu |
| 404 | Nie można odnaleźć elementu lub kolekcji | Sprawdź, czy istnieją identyfikatory |
| 424 | Kafelek poza zakresem danych | Oczekiwano - płynna obsługa |
Rozwiązywanie problemów
| Error | Przyczyna | Rozwiązanie |
|---|---|---|
| "AADSTS50011: Niezgodność adresu URL odpowiedzi" | Identyfikator URI przekierowania w kodzie jest niezgodny z rejestracją Microsoft Entra ID | Dodaj swój deweloperski adres URL (na przykład http://localhost:3000) jako identyfikator URI przekierowania SPA w rejestracji aplikacji |
| Błąd "Nieprawidłowy zakres" | Używanie adresu URL geokatalogu zamiast zakresu interfejsu API | Użyj https://geocatalog.spatio.azure.com/.default jako zakresu |
| 401 Nieautoryzowany dostęp do żądań danych kafelków | Biblioteka map nie zawiera nagłówków uwierzytelniania | Użyj transformRequest (MapLibre), aby dodać Bearer token; upewnij się, że token jest aktualny |
| Kafelki nie są wyrównane z mapą bazową | Niewłaściwy zestaw macierzy kafelków | Użyj tileMatrixSetId=WebMercatorQuad dla projekcji Web Mercator (EPSG:3857) |
| "Nie można dekodować obrazu" | Nieprawidłowa nazwa zasobu, obrazy wielopasmowe lub poza zakresem danych | Sprawdź item_assets prawidłowe nazwy; użyj asset_bidx=image\|1,2,3 dla RGB; oczekiwano pokrycia zewnętrznego 404/424 |