diff --git a/pyproject.toml b/pyproject.toml
index dea8f9a1..7571f887 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -76,6 +76,8 @@ cli = [
stac_server = [
"fastapi[standard]",
"Jinja2",
+ "earthkit-data",
+ "polytope-client",
]
docs = [
diff --git a/stac_server/POLYTOPE_AUTH_TESTING.md b/stac_server/POLYTOPE_AUTH_TESTING.md
new file mode 100644
index 00000000..bd83e217
--- /dev/null
+++ b/stac_server/POLYTOPE_AUTH_TESTING.md
@@ -0,0 +1,47 @@
+# Polytope Authentication
+
+The STAC server's Polytope integration collects authentication credentials directly from users through the web interface and queries the **Destination Earth** Polytope service.
+
+## User Flow
+
+1. Navigate through the STAC catalogue and select your data
+2. At the end of the catalogue, you'll see the "Query Data with Polytope" section
+3. Enter your Destination Earth Polytope credentials:
+ - **Email Address**: Your email address registered with Destination Earth
+ - **API Key**: Your Polytope API key
+4. Click "Query Polytope Service" to submit your data extraction requests
+
+## Getting Your Polytope API Key
+
+1. Visit the [Destination Earth Service Platform (DESP)](https://auth.destine.eu)
+2. Log in with your Destination Earth credentials
+3. Navigate to your profile or settings
+4. Generate or copy your API key
+
+## Technical Details
+
+The service connects to the Destination Earth Polytope instance:
+- **Address**: `polytope.lumi.apps.dte.destination-earth.eu`
+- **Collection**: `destination-earth`
+
+## Security Notes
+
+- Credentials are sent securely with each request
+- Credentials are not stored on the server
+- Each user provides their own authentication
+- Credentials are only logged in a masked format for debugging
+
+## For Developers
+
+The credentials are sent in the request body as:
+```json
+{
+ "requests": [...],
+ "credentials": {
+ "user_email": "user@ecmwf.int",
+ "user_key": "your_api_key"
+ }
+}
+```
+
+The backend passes these credentials directly to `earthkit.data.from_source()` when querying Polytope.
diff --git a/stac_server/main.py b/stac_server/main.py
index 328a6876..bd5a0f6f 100644
--- a/stac_server/main.py
+++ b/stac_server/main.py
@@ -120,6 +120,15 @@ async def deprecated():
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
+ index_config = {
+ "title": os.environ.get("TITLE", "Qubed Catalogue Browser"),
+ }
+
+ return templates.TemplateResponse(request, "landing.html", index_config)
+
+
+@app.get("/browse", response_class=HTMLResponse)
+async def browse_catalogue(request: Request):
index_config = {
"api_url": os.environ.get("API_URL", "/api/v2/"),
"title": os.environ.get("TITLE", "Qubed Catalogue Browser"),
@@ -147,6 +156,120 @@ async def union(
return qube.to_json()
+@app.post("/api/v2/polytope/query")
+async def query_polytope(
+ body_json=Depends(get_body_json),
+):
+ """
+ Query the Destination Earth Polytope data extraction service with MARS requests.
+ Expects a JSON body with:
+ - 'requests': array of MARS request objects
+ - 'credentials': object with 'user_email' and 'user_key' fields
+
+ Connects to: polytope.lumi.apps.dte.destination-earth.eu
+ Collection: destination-earth
+ """
+ try:
+ import earthkit.data
+ except ImportError:
+ raise HTTPException(
+ status_code=500,
+ detail="earthkit.data is not installed. Please install it with 'pip install earthkit-data'",
+ )
+
+ requests = body_json.get("requests", [])
+ if not requests:
+ raise HTTPException(status_code=400, detail="No requests provided")
+
+ # Get credentials from request body
+ credentials = body_json.get("credentials", {})
+ user_email = credentials.get("user_email")
+ user_key = credentials.get("user_key")
+
+ if not user_email or not user_key:
+ raise HTTPException(
+ status_code=400,
+ detail="Credentials required: provide user_email and user_key",
+ )
+
+ # Prepare kwargs for polytope connection
+ polytope_kwargs = {
+ "stream": False,
+ "address": "polytope.lumi.apps.dte.destination-earth.eu",
+ "user_email": user_email,
+ "user_key": user_key,
+ }
+
+ logger.info(f"Querying Polytope with user email: {user_email}")
+
+ results = []
+ successful = 0
+ failed = 0
+
+ for idx, mars_request in enumerate(requests):
+ try:
+ logger.info(f"Querying Polytope for request {idx + 1}/{len(requests)}")
+ logger.debug(f"Request: {mars_request}")
+
+ # Query Polytope service
+ ds = earthkit.data.from_source(
+ "polytope", "destination-earth", mars_request, **polytope_kwargs
+ )
+
+ # Get JSON representation of the data
+ try:
+ ds_json = ds._json()
+ logger.info(f"Successfully extracted JSON from request {idx + 1}")
+ except Exception as json_error:
+ logger.warning(
+ f"Could not extract JSON from request {idx + 1}: {json_error}"
+ )
+ ds_json = None
+
+ # Get some basic info about the result
+ data_info = (
+ f"Retrieved {len(ds)} fields"
+ if hasattr(ds, "__len__")
+ else "Data retrieved"
+ )
+
+ result_entry = {
+ "success": True,
+ "request_index": idx,
+ "message": data_info,
+ "data_size": str(len(ds)) if hasattr(ds, "__len__") else None,
+ "mars_request": mars_request,
+ }
+
+ # Add JSON data if available
+ if ds_json is not None:
+ result_entry["json_data"] = ds_json
+
+ results.append(result_entry)
+ successful += 1
+ logger.info(f"Request {idx + 1} successful: {data_info}")
+
+ except Exception as e:
+ error_msg = str(e)
+ logger.error(f"Request {idx + 1} failed: {error_msg}")
+ results.append(
+ {
+ "success": False,
+ "request_index": idx,
+ "error": error_msg,
+ "mars_request": mars_request,
+ }
+ )
+ failed += 1
+
+ return {
+ "total": len(requests),
+ "successful": successful,
+ "failed": failed,
+ "results": results,
+ }
+
+
def follow_query(request: dict[str, str | list[str]], qube: Qube):
rel_qube = qube.select(request, consume=False)
diff --git a/stac_server/static/app.js b/stac_server/static/app.js
index 8250cc82..23899c84 100644
--- a/stac_server/static/app.js
+++ b/stac_server/static/app.js
@@ -82,7 +82,8 @@ function goToNextUrl() {
);
}
- const any = item.querySelector("input[type='text']");
+ // Get text inputs but exclude the filter input
+ const any = item.querySelector("input[type='text']:not(.filter-input)");
if (any && any.value !== "") {
values.push(any.value);
}
@@ -160,47 +161,238 @@ async function createCatalogItem(link, itemsContainer) {
}
`;
- if (false && key === "date") {
- console.log("Date", variable, exports);
+ if (key === "date" && variable.enum && variable.enum.length > 30) {
+ console.log("Date picker enabled");
+ console.log("First few dates:", variable.enum.slice(0, 10));
- itemDiv.appendChild(toHTML(" "));
- let dates = variable.enum;
+ // Create a unique ID for this date picker
+ const pickerId = `date-picker-${link.title}`;
+ const hiddenInputId = `date-input-${link.title}`;
+
+ itemDiv.appendChild(toHTML(` `));
+ itemDiv.appendChild(toHTML(` `));
+ itemDiv.appendChild(toHTML(`💡 Click a date twice to select it individually, or click two different dates to select a range.
`));
+
+ let dates = variable.enum.map(d => String(d));
itemDiv.querySelector("button.all").style.display = "none";
- let picker = new AirDatepicker("#date-picker", {
+ // Create a set for fast lookup (normalize to YYYY-MM-DD format)
+ const availableDatesSet = new Set(dates);
+ console.log("Available dates set size:", availableDatesSet.size);
+
+ // Parse dates from enum to get min and max dates
+ let parsedDates = dates.map(d => {
+ // Handle both formats: "YYYY-MM-DD" or "YYYYMMDD"
+ const dateStr = String(d);
+ if (dateStr.includes('-')) {
+ return new Date(dateStr);
+ } else {
+ const year = parseInt(dateStr.substring(0, 4));
+ const month = parseInt(dateStr.substring(4, 6)) - 1;
+ const day = parseInt(dateStr.substring(6, 8));
+ return new Date(year, month, day);
+ }
+ });
+
+ let minDate = new Date(Math.min(...parsedDates));
+ let maxDate = new Date(Math.max(...parsedDates));
+
+ console.log("Date range:", minDate.toISOString(), "to", maxDate.toISOString());
+
+ // Track selected dates manually for better control
+ let manuallySelectedDates = new Set();
+ let lastClickedDate = null;
+
+ let picker = new AirDatepicker(`#${pickerId}`, {
position: "bottom center",
inline: true,
locale: exports.default,
- range: true,
- multipleDatesSeparator: " - ",
+ multipleDates: true,
+ multipleDatesSeparator: ",",
+ minDate: minDate,
+ maxDate: maxDate,
+ onSelect({ date, formattedDate, datepicker }) {
+ // Prevent default behavior - we'll handle selection manually
+ },
onRenderCell({ date, cellType }) {
- let isDay = cellType === "day",
- _date =
- String(date.getFullYear()).padStart(4, "0") +
- String(date.getMonth()).padStart(2, "0") +
- String(date.getDate()).padStart(2, "0"),
- shouldChangeContent = isDay && dates.includes(_date);
-
- return {
- classes: shouldChangeContent ? "has-data" : undefined,
- };
+ if (cellType === "day") {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ // Check in the format that matches the input
+ let dateStr;
+ if (dates[0].includes('-')) {
+ dateStr = `${year}-${month}-${day}`;
+ } else {
+ dateStr = `${year}${month}${day}`;
+ }
+ const hasData = availableDatesSet.has(dateStr);
+
+ return {
+ classes: hasData ? "has-data" : "",
+ disabled: !hasData,
+ };
+ }
+ return {};
},
});
+
+ // Custom click handler for date cells
+ const hintElement = document.getElementById(`${pickerId}-hint`);
+
+ // Wait for datepicker to render, then attach event handler
+ setTimeout(() => {
+ const datepickerContainer = document.querySelector(`#${pickerId}`).parentElement.querySelector('.air-datepicker');
+
+ if (datepickerContainer) {
+ datepickerContainer.addEventListener('click', (e) => {
+ const cell = e.target.closest('.air-datepicker-cell.-day-');
+ if (!cell || cell.classList.contains('-disabled-')) return;
+
+ // Get the date from the cell's data attributes
+ const dayNumber = cell.getAttribute('data-date');
+ const monthNumber = cell.getAttribute('data-month');
+ const yearNumber = cell.getAttribute('data-year');
+
+ if (!dayNumber || !monthNumber || !yearNumber) return;
+
+ const cellDate = new Date(parseInt(yearNumber), parseInt(monthNumber), parseInt(dayNumber));
+
+ const formatDate = (d) => {
+ const year = d.getFullYear();
+ const month = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ if (dates[0].includes('-')) {
+ return `${year}-${month}-${day}`;
+ } else {
+ return `${year}${month}${day}`;
+ }
+ };
+
+ const clickedDateStr = formatDate(cellDate);
+
+ // Check if this date has data
+ if (!availableDatesSet.has(clickedDateStr)) return;
+
+ const isSameAsPrevious = lastClickedDate &&
+ cellDate.getTime() === lastClickedDate.getTime();
+
+ if (isSameAsPrevious) {
+ // Clicking same date twice - toggle individual date
+ if (manuallySelectedDates.has(clickedDateStr)) {
+ manuallySelectedDates.delete(clickedDateStr);
+ console.log("Removed date:", clickedDateStr);
+ if (hintElement) hintElement.textContent = `🗑️ Removed ${clickedDateStr}. Total: ${manuallySelectedDates.size} dates selected.`;
+ } else {
+ manuallySelectedDates.add(clickedDateStr);
+ console.log("Added single date:", clickedDateStr);
+ if (hintElement) hintElement.textContent = `✅ Added ${clickedDateStr}. Total: ${manuallySelectedDates.size} dates selected.`;
+ }
+ lastClickedDate = null; // Reset for next selection
+ } else if (lastClickedDate) {
+ // Two different dates clicked - create a range
+ const [startDate, endDate] = [lastClickedDate, cellDate].sort((a, b) => a - b);
+
+ console.log("Creating range from", formatDate(startDate), "to", formatDate(endDate));
+
+ let currentDate = new Date(startDate);
+ const rangeEnd = new Date(endDate);
+ let rangeCount = 0;
+
+ while (currentDate <= rangeEnd) {
+ const dateStr = formatDate(currentDate);
+ if (availableDatesSet.has(dateStr)) {
+ manuallySelectedDates.add(dateStr);
+ rangeCount++;
+ }
+ currentDate.setDate(currentDate.getDate() + 1);
+ }
+
+ console.log("Range added, total dates selected:", manuallySelectedDates.size);
+ if (hintElement) hintElement.textContent = `📅 Added range: ${rangeCount} dates. Total: ${manuallySelectedDates.size} dates selected.`;
+ lastClickedDate = null; // Reset for next selection
+ } else {
+ // First click of a potential range
+ lastClickedDate = cellDate;
+ console.log("First date clicked for range:", clickedDateStr);
+ if (hintElement) hintElement.textContent = `🎯 First date selected: ${clickedDateStr}. Click another date to create a range, or click this date again to select it individually.`;
+ return; // Don't update selection yet, wait for second click
+ }
+
+ // Update the visual selection in datepicker
+ const selectedDateObjects = Array.from(manuallySelectedDates).map(dateStr => {
+ if (dateStr.includes('-')) {
+ return new Date(dateStr);
+ } else {
+ const year = parseInt(dateStr.substring(0, 4));
+ const month = parseInt(dateStr.substring(4, 6)) - 1;
+ const day = parseInt(dateStr.substring(6, 8));
+ return new Date(year, month, day);
+ }
+ });
+
+ picker.selectDate(selectedDateObjects);
+
+ // Update hidden input
+ const hiddenInput = document.getElementById(hiddenInputId);
+ hiddenInput.value = Array.from(manuallySelectedDates).join(',');
+ console.log("Total selected dates:", manuallySelectedDates.size);
+ console.log("Selected dates:", hiddenInput.value.split(',').slice(0, 10).join(', '), '...');
+ });
+ }
+ }, 100);
+
+ console.log("Datepicker initialized");
} else if (variable.enum && variable.enum.length > 0) {
+ // Add filter input at the top if there are many options
+ if (variable.enum.length > 5) {
+ const filterWrapper = toHTML(`
+
+
+
+ `);
+ itemDiv.appendChild(filterWrapper);
+ }
+
+ // Add checkbox list
const checkbox_list = renderCheckboxList(link);
itemDiv.appendChild(checkbox_list);
+ // Add filter functionality if filter exists
+ if (variable.enum.length > 5) {
+ const filterInput = itemDiv.querySelector(`#filter-${link.title}`);
+ if (filterInput) {
+ filterInput.addEventListener('input', (e) => {
+ const filterText = e.target.value.toLowerCase();
+ const checkboxRows = checkbox_list.querySelectorAll('.checkbox-row');
+
+ checkboxRows.forEach(row => {
+ const label = row.querySelector('label');
+ const code = row.querySelector('label.code code');
+ const labelText = label ? label.textContent.toLowerCase() : '';
+ const codeText = code ? code.textContent.toLowerCase() : '';
+
+ if (labelText.includes(filterText) || codeText.includes(filterText)) {
+ row.style.display = '';
+ } else {
+ row.style.display = 'none';
+ }
+ });
+ });
+ }
+ }
+
itemDiv.querySelector("button.all").addEventListener("click", () => {
let new_state;
if (checkbox_list.hasAttribute("disabled")) {
checkbox_list.removeAttribute("disabled");
- itemDiv.querySelectorAll("input").forEach((c) => {
+ itemDiv.querySelectorAll("input[type='checkbox']").forEach((c) => {
c.removeAttribute("checked");
c.removeAttribute("disabled");
});
} else {
checkbox_list.setAttribute("disabled", "");
- itemDiv.querySelectorAll("input").forEach((c) => {
+ itemDiv.querySelectorAll("input[type='checkbox']").forEach((c) => {
c.setAttribute("checked", "true");
c.setAttribute("disabled", "");
});
@@ -277,59 +469,82 @@ function renderCatalogItems(links) {
function renderRequestBreakdown(request, descriptions) {
const container = document.getElementById("request-breakdown");
const format_value = (key, value) => {
- return `"${value}" `;
+ return `" ${value} " `;
};
const format_values = (key, values) => {
if (values.length === 1) {
return format_value(key, values[0]);
}
- return `[${values.map((v) => format_value(key, v)).join(", ")}]`;
+ return `[ ${values.map((v) => format_value(key, v)).join(`, `)}] `;
};
let html =
- `{\n` +
+ `{ \n` +
request
.map(
([key, values]) =>
- ` "${key}" : ${format_values(key, values)},`
+ ` " ${key} " : ${format_values(key, values)}, `
)
.join("\n") +
- `\n}`;
+ `\n} `;
container.innerHTML = html;
}
function renderMARSRequest(request, descriptions) {
const container = document.getElementById("final_req");
const format_value = (key, value) => {
- return `"${value}" `;
+ return `" ${value} " `;
};
const format_values = (key, values) => {
if (values.length === 1) {
return format_value(key, values[0]);
}
- return `[${values.map((v) => format_value(key, v)).join(", ")}]`;
+ return `[ ${values.map((v) => format_value(key, v)).join(`, `)}] `;
};
+ // Add feature object to each request if polygon is selected
+ const requestsWithFeature = selectedPolygon ? request.map(obj => ({
+ ...obj,
+ feature: {
+ type: "polygon",
+ shape: selectedPolygon
+ }
+ })) : request;
+
+ // Store for copying
+ currentMARSRequests = requestsWithFeature;
+
let html =
- `[\n` +
- request
+ `[ \n` +
+ requestsWithFeature
.map(
- obj =>
- ` {\n` +
- Object.entries(obj)
+ obj => {
+ const entries = Object.entries(obj);
+ return ` { \n` +
+ entries
.map(
- ([key, values]) =>
- ` "${key}" : ${format_values(key, values)},`
+ ([key, values], idx) => {
+ const isLast = idx === entries.length - 1;
+ if (key === "feature" && values && typeof values === "object" && values.type === "polygon") {
+ // Format the feature object specially
+ const shapeStr = JSON.stringify(values.shape, null, 0);
+ return ` " feature " : { \n` +
+ ` " type " : " ${values.type} " , \n` +
+ ` " shape " : ${shapeStr} \n` +
+ ` } ${isLast ? '' : ', '}`;
+ }
+ return ` " ${key} " : ${format_values(key, values)}${isLast ? '' : ', '}`;
+ }
)
.join("\n") +
- `\n }`
+ `\n } `;
+ }
)
- .join(",\n") +
- `\n]`;
+ .join(`, \n`) +
+ `\n] `;
container.innerHTML = html;
}
@@ -353,9 +568,28 @@ async function fetchCatalog(request, stacUrl) {
const response = await fetch(stacUrl);
const catalog = await response.json();
- // Render the request breakdown in the sidebar
- renderRequestBreakdown(request, catalog.debug.descriptions);
- renderMARSRequest(catalog.final_object, catalog.debug.descriptions);
+ // Check if we've reached the end of the catalogue (final_object has data)
+ const hasReachedEnd = catalog.final_object && catalog.final_object.length > 0;
+
+ // Get section elements
+ const currentSelectionSection = document.getElementById("current-selection-section");
+ const marsRequestsSection = document.getElementById("mars-requests-section");
+ const nextButton = document.getElementById("next-btn");
+
+ if (hasReachedEnd) {
+ // At the end: show MARS requests, hide current selection and next button
+ currentSelectionSection.style.display = "none";
+ marsRequestsSection.style.display = "block";
+ nextButton.style.display = "none";
+ catalogCache = catalog; // Store catalog for re-rendering with features
+ renderMARSRequest(catalog.final_object, catalog.debug.descriptions);
+ } else {
+ // Not at the end: show current selection, hide MARS requests, show next button
+ currentSelectionSection.style.display = "block";
+ marsRequestsSection.style.display = "none";
+ nextButton.style.display = "flex";
+ renderRequestBreakdown(request, catalog.debug.descriptions);
+ }
// Show the raw STAC in the sidebar
renderRawSTACResponse(catalog);
@@ -366,6 +600,20 @@ async function fetchCatalog(request, stacUrl) {
renderCatalogItems(catalog.links);
}
+ // Show region selection at the end of catalogue
+ const regionSelection = document.getElementById("region-selection");
+ const catalogList = document.getElementById("catalog-list");
+ const polytopeSection = document.getElementById("polytope-section");
+ if (hasReachedEnd) {
+ regionSelection.style.display = "block";
+ catalogList.classList.add("region-active");
+ if (polytopeSection) polytopeSection.style.display = "block";
+ } else {
+ regionSelection.style.display = "none";
+ catalogList.classList.remove("region-active");
+ if (polytopeSection) polytopeSection.style.display = "none";
+ }
+
// Highlight the request and raw STAC
hljs.highlightElement(document.getElementById("raw-stac"));
hljs.highlightElement(document.getElementById("debug"));
@@ -399,5 +647,331 @@ function initializeViewer() {
stacAnchor.href = getSTACUrlFromQuery();
}
+// Copy MARS requests to clipboard
+function copyMARSRequests() {
+ const copyBtn = document.getElementById("copy-mars-btn");
+ const btnText = copyBtn.querySelector(".copy-btn-text");
+
+ // Use the stored MARS requests with feature if available
+ const jsonContent = JSON.stringify(currentMARSRequests, null, 2);
+
+ navigator.clipboard.writeText(jsonContent).then(() => {
+ // Change button text temporarily
+ btnText.textContent = "Copied!";
+ copyBtn.classList.add("copied");
+
+ // Reset after 2 seconds
+ setTimeout(() => {
+ btnText.textContent = "Copy";
+ copyBtn.classList.remove("copied");
+ }, 2000);
+ }).catch(err => {
+ console.error("Failed to copy:", err);
+ btnText.textContent = "Failed";
+ setTimeout(() => {
+ btnText.textContent = "Copy";
+ }, 2000);
+ });
+}
+
+// Download JSON data as a file
+function downloadJSON(data, filename) {
+ const jsonString = JSON.stringify(data, null, 2);
+ const blob = new Blob([jsonString], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+}
+
+// ============================================
+// Geographic Region Selection with Map
+// ============================================
+
+let regionMap = null;
+let drawnItems = null;
+let selectedPolygon = null;
+let currentMARSRequests = []; // Store current MARS requests for copying
+let catalogCache = null; // Store catalog for re-rendering when polygon changes
+
+function initializeRegionMap() {
+ const mapElement = document.getElementById('map');
+ if (!mapElement || regionMap) return;
+
+ // Initialize map centered on the world
+ regionMap = L.map('map').setView([20, 0], 2);
+
+ // Add OpenStreetMap tile layer
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors',
+ maxZoom: 18,
+ }).addTo(regionMap);
+
+ // Initialize the FeatureGroup to store editable layers
+ drawnItems = new L.FeatureGroup();
+ regionMap.addLayer(drawnItems);
+
+ // Initialize the draw control
+ const drawControl = new L.Control.Draw({
+ position: 'topright',
+ draw: {
+ polyline: false,
+ circle: false,
+ circlemarker: false,
+ marker: false,
+ rectangle: true,
+ polygon: {
+ allowIntersection: false,
+ showArea: true,
+ shapeOptions: {
+ color: '#0066cc',
+ weight: 2,
+ fillOpacity: 0.2
+ }
+ }
+ },
+ edit: {
+ featureGroup: drawnItems,
+ remove: true
+ }
+ });
+ regionMap.addControl(drawControl);
+
+ // Handle polygon creation
+ regionMap.on('draw:created', function (e) {
+ // Clear previous polygons
+ drawnItems.clearLayers();
+
+ const layer = e.layer;
+ drawnItems.addLayer(layer);
+
+ // Get the coordinates
+ const coordinates = layer.getLatLngs()[0].map(latlng => [
+ parseFloat(latlng.lat.toFixed(6)),
+ parseFloat(latlng.lng.toFixed(6))
+ ]);
+
+ // Close the polygon by adding the first point at the end
+ coordinates.push(coordinates[0]);
+
+ selectedPolygon = coordinates;
+ displaySelectedRegion(coordinates);
+ });
+
+ // Handle polygon edit
+ regionMap.on('draw:edited', function (e) {
+ const layers = e.layers;
+ layers.eachLayer(function (layer) {
+ const coordinates = layer.getLatLngs()[0].map(latlng => [
+ parseFloat(latlng.lat.toFixed(6)),
+ parseFloat(latlng.lng.toFixed(6))
+ ]);
+ coordinates.push(coordinates[0]);
+ selectedPolygon = coordinates;
+ displaySelectedRegion(coordinates);
+ });
+ });
+
+ // Handle polygon deletion
+ regionMap.on('draw:deleted', function (e) {
+ selectedPolygon = null;
+ document.getElementById('selected-region').style.display = 'none';
+ });
+
+ // Force map to resize properly
+ setTimeout(() => {
+ regionMap.invalidateSize();
+ }, 100);
+}
+
+function displaySelectedRegion(coordinates) {
+ const selectedRegionDiv = document.getElementById('selected-region');
+ const coordinatesDisplay = document.getElementById('region-coordinates');
+
+ const regionFeature = {
+ type: "polygon",
+ shape: coordinates
+ };
+
+ coordinatesDisplay.textContent = JSON.stringify({ feature: regionFeature }, null, 2);
+ selectedRegionDiv.style.display = 'block';
+
+ // Re-render MARS requests with the feature appended
+ if (catalogCache && catalogCache.final_object) {
+ renderMARSRequest(catalogCache.final_object, catalogCache.debug.descriptions);
+ }
+}
+
+// Event listeners for region selection
+document.addEventListener("DOMContentLoaded", () => {
+ const enableRegionBtn = document.getElementById('enable-region-btn');
+ const skipRegionBtn = document.getElementById('skip-region-btn');
+ const clearRegionBtn = document.getElementById('clear-region-btn');
+ const mapContainer = document.getElementById('map-container');
+
+ if (enableRegionBtn) {
+ enableRegionBtn.addEventListener('click', () => {
+ mapContainer.style.display = 'block';
+ enableRegionBtn.style.display = 'none';
+ skipRegionBtn.textContent = 'Continue Without Region';
+ initializeRegionMap();
+ });
+ }
+
+ if (skipRegionBtn) {
+ skipRegionBtn.addEventListener('click', () => {
+ // User chose to skip region selection - could proceed to next step
+ console.log('User skipped region selection');
+ // Here you could trigger the next action or inform the user
+ });
+ }
+
+ if (clearRegionBtn) {
+ clearRegionBtn.addEventListener('click', () => {
+ if (drawnItems) {
+ drawnItems.clearLayers();
+ }
+ selectedPolygon = null;
+ document.getElementById('selected-region').style.display = 'none';
+
+ // Re-render MARS requests without the feature
+ if (catalogCache && catalogCache.final_object) {
+ renderMARSRequest(catalogCache.final_object, catalogCache.debug.descriptions);
+ }
+ });
+ }
+});
+
+// ============================================
+// Polytope Query Handler
+// ============================================
+
+async function queryPolytope() {
+ const polytopeBtn = document.getElementById('polytope-btn');
+ const polytopeBtnText = document.getElementById('polytope-btn-text');
+ const polytopeStatus = document.getElementById('polytope-status');
+ const polytopeResults = document.getElementById('polytope-results');
+ const emailInput = document.getElementById('polytope-email');
+ const keyInput = document.getElementById('polytope-key');
+
+ if (!currentMARSRequests || currentMARSRequests.length === 0) {
+ polytopeStatus.textContent = 'No MARS requests available to query.';
+ polytopeStatus.className = 'polytope-status error';
+ polytopeStatus.style.display = 'block';
+ return;
+ }
+
+ // Validate credentials
+ const email = emailInput.value.trim();
+ const apiKey = keyInput.value.trim();
+
+ if (!email || !apiKey) {
+ polytopeStatus.textContent = 'Please provide both email and API key to query Polytope.';
+ polytopeStatus.className = 'polytope-status error';
+ polytopeStatus.style.display = 'block';
+ return;
+ }
+
+ // Disable button and show loading state
+ polytopeBtn.disabled = true;
+ polytopeBtnText.textContent = 'Querying...';
+ polytopeStatus.textContent = `Submitting ${currentMARSRequests.length} request(s) to Polytope service...`;
+ polytopeStatus.className = 'polytope-status loading';
+ polytopeStatus.style.display = 'block';
+ polytopeResults.innerHTML = '';
+ polytopeResults.style.display = 'none';
+
+ try {
+ const response = await fetch('/api/v2/polytope/query', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ requests: currentMARSRequests,
+ credentials: {
+ user_email: email,
+ user_key: apiKey
+ }
+ }),
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(result.detail || 'Failed to query Polytope service');
+ }
+
+ // Show success message
+ polytopeStatus.textContent = `Successfully submitted ${result.total} request(s). ${result.successful} succeeded, ${result.failed} failed.`;
+ polytopeStatus.className = 'polytope-status success';
+
+ // Display detailed results
+ if (result.results && result.results.length > 0) {
+ polytopeResults.innerHTML = result.results.map((res, idx) => `
+
+
+
+ ${res.success
+ ? `Data retrieved successfully${res.data_size ? ` (${res.data_size})` : ''}`
+ : `Error: ${res.error || 'Unknown error'}`
+ }
+
+ ${res.message ? `
${res.message}
` : ''}
+ ${res.success && res.json_data ? `
+
+ 📥 Download JSON
+
+ ` : ''}
+
+ `).join('');
+ polytopeResults.style.display = 'block';
+
+ // Add event listeners to download buttons
+ document.querySelectorAll('.download-json-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const idx = parseInt(e.target.getAttribute('data-request-idx'));
+ const resultData = result.results[idx];
+ if (resultData && resultData.json_data) {
+ downloadJSON(resultData.json_data, `polytope_request_${idx + 1}.json`);
+ }
+ });
+ });
+ }
+
+ polytopeBtnText.textContent = 'Query Complete';
+ } catch (error) {
+ console.error('Polytope query error:', error);
+ polytopeStatus.textContent = `Error: ${error.message}`;
+ polytopeStatus.className = 'polytope-status error';
+ } finally {
+ // Re-enable button after a delay
+ setTimeout(() => {
+ polytopeBtn.disabled = false;
+ polytopeBtnText.textContent = 'Query Polytope Service';
+ }, 2000);
+ }
+}
+
// Call initializeViewer on page load
initializeViewer();
+
+// Add event listener for copy button
+document.addEventListener("DOMContentLoaded", () => {
+ const copyBtn = document.getElementById("copy-mars-btn");
+ if (copyBtn) {
+ copyBtn.addEventListener("click", copyMARSRequests);
+ }
+
+ // Add event listener for Polytope button
+ const polytopeBtn = document.getElementById('polytope-btn');
+ if (polytopeBtn) {
+ polytopeBtn.addEventListener('click', queryPolytope);
+ }
+});
diff --git a/stac_server/static/logo.png b/stac_server/static/logo.png
new file mode 100644
index 00000000..5c449526
Binary files /dev/null and b/stac_server/static/logo.png differ
diff --git a/stac_server/static/logo_old.svg b/stac_server/static/logo_old.svg
new file mode 100644
index 00000000..b8e4bc8c
--- /dev/null
+++ b/stac_server/static/logo_old.svg
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/stac_server/static/styles.css b/stac_server/static/styles.css
index 81202e3b..31f2c144 100644
--- a/stac_server/static/styles.css
+++ b/stac_server/static/styles.css
@@ -1,303 +1,1398 @@
+/* ============================================
+ Modern Professional Catalogue Styles
+ ============================================ */
+
+/* CSS Variables for consistent theming */
+:root {
+ --primary-color: #0073E6;
+ --primary-dark: #2F3842;
+ --primary-light: #009DEB;
+ --secondary-color: #009DEB;
+ --accent-color: #FF6B9D;
+
+ --bg-primary: #ffffff;
+ --bg-secondary: #F6F9FC;
+ --bg-tertiary: #F2F7FD;
+ --bg-dark: #2F3842;
+
+ --text-primary: #100F0F;
+ --text-secondary: #B1B5C3;
+ --text-light: #adb5bd;
+ --text-inverse: #ffffff;
+
+ --border-color: #dee2e6;
+ --border-light: #e9ecef;
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.12);
+ --shadow-hover: 0 8px 16px rgba(0, 0, 0, 0.15);
+
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 14px;
+
+ --transition-fast: 0.15s ease;
+ --transition-base: 0.25s ease;
+ --transition-slow: 0.35s ease;
+
+ --header-height: 70px;
+ --sidebar-width: 420px;
+}
+
+/* Reset & Base Styles */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
html,
body {
min-height: 100vh;
height: 100%;
-
- --accent-color: #003399;
- --background-grey: #f4f4f4;
+ margin: 0;
+ padding: 0;
}
body {
- font-family: Arial, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ font-size: 15px;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background-color: var(--bg-secondary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* ============================================
+ Header Styles
+ ============================================ */
+
+.main-header {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
+ color: var(--text-inverse);
+ box-shadow: var(--shadow-md);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+ height: var(--header-height);
+}
+
+.header-content {
+ max-width: 1800px;
+ margin: 0 auto;
+ padding: 0 2rem;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.logo-icon {
+ font-size: 2rem;
+ line-height: 1;
+}
+
+.site-title {
+ font-size: 1.5rem;
+ font-weight: 600;
margin: 0;
- padding-left: 0.5em;
- padding-right: 0.5em;
+ letter-spacing: -0.02em;
+}
+
+.header-right {
+ display: flex;
+ gap: 1rem;
+}
+
+.header-link {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: var(--radius-sm);
+ color: var(--text-inverse);
+ text-decoration: none;
+ font-weight: 500;
+ transition: background var(--transition-base);
+}
+.header-link:hover {
+ background: rgba(255, 255, 255, 0.2);
}
+/* ============================================
+ Layout
+ ============================================ */
#viewer {
display: flex;
flex-direction: row;
- height: fit-content;
- min-height: 100vh;
+ min-height: calc(100vh - var(--header-height));
+ max-width: 1800px;
+ margin: 0 auto;
}
+/* ============================================
+ Sidebar / Catalog List
+ ============================================ */
+
#catalog-list {
- max-width: 400px;
- padding: 10px;
- overflow-y: scroll;
- background-color: var(--background-grey);
- border-right: 1px solid #ddd;
+ flex: 0 0 var(--sidebar-width);
+ background-color: var(--bg-primary);
+ border-right: 1px solid var(--border-color);
+ overflow-y: auto;
+ box-shadow: var(--shadow-sm);
+ transition: flex-basis var(--transition-base);
}
-#catalog-list h2 {
- margin-top: 0;
+#catalog-list.region-active {
+ flex: 0 0 650px;
}
-#details {
- width: 70%;
- padding: 10px;
+.sidebar-sticky {
+ padding: 2rem 1.5rem;
+}
+
+.sidebar-intro {
+ margin-bottom: 2rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 2px solid var(--border-light);
}
-.sidebar-header {
+.instruction-text {
+ margin: 0 0 0.75rem 0;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+.update-time {
display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 1rem 0 0 0;
+ padding: 0.75rem 1rem;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+
+/* ============================================
+ Navigation Controls
+ ============================================ */
+
+.sidebar-nav {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.nav-btn {
+ flex: 1;
+ display: flex;
+ align-items: center;
justify-content: center;
- margin-bottom: 10px;
- flex-wrap: wrap;
- gap: 0.5em;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-sm);
}
-.sidebar-header button {
- width: 7em;
- height: 2em;
- padding: 0;
+.nav-btn.primary {
+ background: var(--primary-color);
+ color: var(--text-inverse);
}
-canvas {
- width: 100%;
- height: 300px;
- border: 1px solid #ccc;
- margin-top: 20px;
+.nav-btn.primary:hover {
+ background: var(--primary-dark);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-1px);
+}
+
+.nav-btn.secondary {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.nav-btn.secondary:hover {
+ background: var(--border-color);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-1px);
+}
+
+.nav-btn:active {
+ transform: translateY(0);
+}
+
+/* ============================================
+ Catalog Items / Cards
+ ============================================ */
+
+.catalog-items {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+.catalog-items {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
}
-/* Updated CSS for the item elements in the catalog list */
.item {
+ position: relative;
display: flex;
flex-direction: column;
+ background: var(--bg-primary);
+ border: 2px solid var(--border-light);
+ border-radius: var(--radius-lg);
+ padding: 1.5rem;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-sm);
+}
- background-color: white;
- border: 1px solid #ddd;
- padding: 10px;
- margin-bottom: 10px;
- border-radius: 5px;
- transition: background-color 0.2s ease;
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+.item:hover {
+ border-color: var(--primary-light);
+ box-shadow: var(--shadow-hover);
+ transform: translateY(-2px);
+}
+
+.item.selected {
+ background: linear-gradient(135deg, #F2F7FD 0%, #F6F9FC 100%);
+ border-color: var(--primary-color);
+ box-shadow: var(--shadow-md);
}
.item-title {
- font-size: 18px;
- margin: 0;
- color: #333;
+ font-size: 1.15rem;
+ font-weight: 600;
+ margin: 0 0 0.5rem 0;
+ color: var(--text-primary);
+ letter-spacing: -0.01em;
}
.item-type {
- font-size: 14px;
- margin: 5px 0;
- color: #666;
+ display: inline-block;
+ padding: 0.25rem 0.75rem;
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ font-weight: 500;
+ border-radius: var(--radius-sm);
+ margin-bottom: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
}
-.item-id,
-.item-key-type {
- font-size: 12px;
- color: #999;
+.item-description {
+ font-size: 0.9rem;
+ margin: 0.5rem 0 1rem 0;
+ color: var(--text-secondary);
+ line-height: 1.5;
}
-.item-description {
- font-size: 13px;
- margin: 5px 0;
- color: #444;
- font-style: italic;
+/* Select All Button */
+.item button.all {
+ position: absolute;
+ right: 1.25rem;
+ top: 1.25rem;
+ width: 2rem;
+ height: 2rem;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 2px solid var(--border-color);
+ border-radius: 50%;
+ font-size: 1.2rem;
+ font-weight: bold;
+ cursor: pointer;
+ transition: all var(--transition-fast);
}
-.item.selected {
- background-color: var(--background-grey);
- border-color: var(--accent-color);
+.item button.all:hover {
+ background: var(--primary-color);
+ color: var(--text-inverse);
+ border-color: var(--primary-color);
+ transform: scale(1.1);
}
+/* ============================================
+ Checkbox Container
+ ============================================ */
-summary {
- h2 {
- display: inline;
- }
+.checkbox-container {
+ display: grid;
+ grid-template-columns: auto auto 1fr;
+ grid-auto-rows: auto;
+ grid-column-gap: 0.75rem;
+ grid-row-gap: 0.75rem;
+ max-height: 280px;
+ overflow-y: auto;
+ padding: 1rem;
+ margin-top: 1rem;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius-md);
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-color) transparent;
+}
+
+.checkbox-container::-webkit-scrollbar {
+ width: 6px;
}
-details[open] summary:has(> h2) {
- margin-bottom: 0.5em;
+
+.checkbox-container::-webkit-scrollbar-track {
+ background: transparent;
}
-.json-pre {
- white-space: pre-wrap;
- /* background-color: #f9f9f9; */
- border: 1px solid #ccc;
- border-radius: 5px;
- padding: 10px;
+.checkbox-container::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 3px;
}
+.checkbox-container::-webkit-scrollbar-thumb:hover {
+ background: var(--text-secondary);
+}
-/* Button styles */
-button {
- height: 3em;
- padding: 10px 20px;
- /* Padding around button text */
- margin: 0 5px;
- /* Margin between buttons */
- background-color: var(--accent-color);
- /* ECMWF blue */
- color: white;
- /* White text color */
- border: none;
- /* Remove default button border */
+.checkbox-container[disabled] {
+ background-color: #e9ecef;
+ opacity: 0.7;
+}
+
+.checkbox-row {
+ display: contents;
+}
+
+.checkbox-row:hover > * {
+ color: var(--primary-color);
+}
+
+.checkbox-row:hover > input[type='checkbox'] {
+ box-shadow: 0 0 0 3px rgba(0, 115, 230, 0.15);
+}
+
+.checkbox-row input[type='checkbox'] {
+ grid-column-start: 1;
+ width: 1.25rem;
+ height: 1.25rem;
+ align-self: center;
cursor: pointer;
- /* Pointer cursor on hover */
- border-radius: 5px;
- /* Rounded corners */
- transition: background-color 0.3s ease;
- /* Smooth background color transition */
+ accent-color: var(--primary-color);
+ transition: all var(--transition-fast);
}
-button:hover {
- background-color: #001f66;
+.checkbox-row a.more-info {
+ cursor: help;
+ text-decoration: none;
+ font-size: 0.7rem;
+ font-weight: 600;
+ display: inline-flex;
+ width: 1.2rem;
+ height: 1.2rem;
+ padding: 0;
+ align-items: center;
+ justify-content: center;
+ aspect-ratio: 1 / 1;
+ border-radius: 50%;
+ border: 2px solid var(--text-secondary);
+ color: var(--text-secondary);
+ transition: all var(--transition-fast);
}
+.checkbox-row a.more-info:hover {
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+ color: var(--text-inverse);
+ transform: scale(1.15);
+}
-.item {
- position: relative;
+.checkbox-row label {
+ grid-column-start: 2;
+ align-self: center;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: color var(--transition-fast);
+}
- button.all {
- position: absolute;
- right: 10px;
- top: 10px;
- text-align: right;
- padding: 0;
- width: 1em;
- height: 1em;
- color: black;
- background-color: transparent;
- border-radius: 50%;
- font-size: 2em;
- }
+.checkbox-row label.code {
+ grid-column-start: 3;
+ text-align: right;
+ align-self: center;
+ font-family: 'Monaco', 'Courier New', monospace;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
- .checkbox-container[disabled] {
- background-color: #d3d3d3;
- }
+/* ============================================
+ Filter Input for Checkbox Lists
+ ============================================ */
- .checkbox-container {
- display: grid;
- grid-template-columns: auto auto 1fr;
- grid-auto-rows: auto;
- grid-column-gap: 10px;
- grid-row-gap: 10px;
-
- margin-top: 20px;
- max-height: 200px;
- overflow-y: auto;
- padding: 10px;
- border: 1px solid #ccc;
- border-radius: 4px;
- background-color: #fff;
-
- div.checkbox-row:hover > * {
- color: var(--accent-color);
- &[type='checkbox'] {box-shadow: 0px 0px 5px var(--accent-color);}
- }
-
- div.checkbox-row {
- display: contents;
-
- /* Set the checkbox checked colour */
- input[type=checkbox] {
- accent-color: var(--accent-color);
- }
-
- input[type='checkbox'] {
- grid-column-start: 1;
- height: 1.5em;
- width: 1.5em;
- align-self: center;
- cursor: pointer;
- }
-
- a.more-info {
- cursor: help;
- text-decoration: none;
- font-size: 0.7em;
- display: inline-flex;
- width: fit-content;
- min-width: 1em;
- padding: 0.1em;
- align-items: center;
- justify-content: center;
- aspect-ratio: 1 / 1;
- border-radius: 50%;
- border: 2px solid #666;
- }
-
- label {
- grid-column-start: 2;
- align-self: center;
- font-size: 16px;
- color: #333;
- }
-
- label.code {
- grid-column-start: 3;
- text-align: right;
- align-self: center;
- }
-
- }
- }
+.filter-wrapper {
+ margin-top: 1rem;
+ margin-bottom: 0.75rem;
+}
- .checkbox-container:hover .key-value {
- color: var(--accent-color);
- }
+.filter-input {
+ width: 100%;
+ padding: 0.625rem 0.875rem 0.625rem 2.25rem;
+ border: 2px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ transition: all var(--transition-base);
+ background-image: url('data:image/svg+xml, ');
+ background-repeat: no-repeat;
+ background-position: 0.75rem center;
+ background-size: 16px 16px;
}
-.list-label {
- font-weight: bold;
- margin-bottom: 0.5em;
- display: block;
- color: var(--accent-color);
+.filter-input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(0, 115, 230, 0.1);
}
-span.key,
-span.value {
- color: #ba2121;
- ;
+.filter-input::placeholder {
+ color: var(--text-light);
+}
+
+/* Sidebar Footer */
+.sidebar-footer {
+ margin-top: 2rem;
+ padding-top: 1.5rem;
+ border-top: 2px solid var(--border-light);
+}
+
+.text-link {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 500;
+ font-size: 0.9rem;
+ transition: all var(--transition-base);
+}
+
+.text-link:hover {
+ background: var(--primary-color);
+ color: var(--text-inverse);
+ box-shadow: var(--shadow-md);
+}
+
+/* ============================================
+ Main Content Area
+ ============================================ */
+
+#details {
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+ background: var(--bg-secondary);
+}
+
+.detail-section {
+ background: var(--bg-primary);
+ border-radius: var(--radius-lg);
+ padding: 2rem;
+ margin-bottom: 1.5rem;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border-light);
+}
+
+.detail-section.collapsible {
+ padding: 0;
+}
+
+.detail-section.collapsible summary {
+ padding: 1.5rem 2rem;
+ cursor: pointer;
+ transition: background var(--transition-base);
+ border-radius: var(--radius-lg);
+}
+
+.detail-section.collapsible summary:hover {
+ background: var(--bg-secondary);
+}
+
+.detail-section.collapsible[open] summary {
+ border-bottom: 1px solid var(--border-light);
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+}
+
+.detail-section.collapsible > *:not(summary) {
+ padding: 0 2rem 2rem 2rem;
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0 0 1rem 0;
+ color: var(--text-primary);
+ letter-spacing: -0.02em;
+}
+
+.section-title svg {
+ color: var(--primary-color);
+}
+
+.section-description {
+ margin: 0 0 1.25rem 0;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.6;
+}
+
+.section-description a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color var(--transition-fast);
+}
+
+.section-description a:hover {
+ color: var(--primary-dark);
+ text-decoration: underline;
+}
+
+/* ============================================
+ Code Blocks
+ ============================================ */
+
+.code-block {
+ background: var(--bg-dark);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.code-block pre {
+ margin: 0;
+ padding: 1.5rem;
+ overflow-x: auto;
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+ font-size: 0.85rem;
+ line-height: 1.6;
+ scrollbar-width: thin;
+ scrollbar-color: var(--text-secondary) transparent;
}
+.code-block pre::-webkit-scrollbar {
+ height: 8px;
+}
+
+.code-block pre::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.code-block pre::-webkit-scrollbar-thumb {
+ background: var(--text-secondary);
+ border-radius: 4px;
+}
+
+.code-block code {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+#final_req,
+#qube {
+ margin: 0;
+ padding: 1.5rem;
+ background: var(--bg-dark);
+ color: #e9ecef;
+ border-radius: var(--radius-md);
+ overflow-x: auto;
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
+ font-size: 0.85rem;
+ line-height: 1.6;
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+/* Syntax Highlighting Enhancements */
span.key {
- font-weight: bold;
+ color: #4fc3f7;
+ font-weight: 600;
+}
+
+span.value {
+ color: #81c784;
}
span.key:hover,
span.value:hover {
- color: #ff2a2a;
+ color: #ffeb3b;
+ cursor: help;
+}
+
+span.punct {
+ color: #e0e0e0;
+ font-weight: 500;
+}
+
+/* ============================================
+ Form Elements
+ ============================================ */
+
+input[type="text"] {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--border-color);
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+ font-family: inherit;
+ transition: all var(--transition-base);
+ background: var(--bg-primary);
+}
+
+input[type="text"]:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(0, 115, 230, 0.1);
+}
+
+/* ============================================
+ Utility Classes
+ ============================================ */
+
+.has-data {
+ background-color: rgba(129, 199, 132, 0.2);
+ border: 2px solid #81c784 !important;
+}
+
+.list-label {
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+ display: block;
+ color: var(--primary-color);
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+/* Date Picker Styles */
+.date-picker-input {
+ width: 100%;
+ margin-top: 1rem;
+ border: none;
+ background: transparent;
+}
+
+.date-picker-hint {
+ margin: 0.75rem 0 1rem 0;
+ padding: 0.75rem 1rem;
+ background: linear-gradient(135deg, #F2F7FD 0%, #F6F9FC 100%);
+ border-left: 4px solid var(--primary-color);
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+ line-height: 1.5;
+ color: var(--text-secondary);
+ font-weight: 500;
+ transition: all var(--transition-base);
+}
+
+.date-picker-hint:empty {
+ display: none;
+}
+
+div.air-datepicker {
+ width: 100% !important;
+ background: var(--bg-primary);
+ border: 2px solid var(--border-light);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ font-family: inherit;
+}
+
+div.air-datepicker .air-datepicker-nav {
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-light);
+ padding: 0.75rem;
+}
+
+div.air-datepicker .air-datepicker-nav--title {
+ color: var(--text-primary);
+ font-weight: 600;
+}
+
+div.air-datepicker .air-datepicker-nav--action {
+ color: var(--primary-color);
+}
+
+div.air-datepicker .air-datepicker-nav--action:hover {
+ background: var(--primary-light);
+}
+
+div.air-datepicker .air-datepicker-body--day-name {
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+}
+
+div.air-datepicker .air-datepicker-cell {
+ color: var(--text-primary);
+ border-radius: var(--radius-sm);
+}
+
+div.air-datepicker .air-datepicker-cell.-disabled- {
+ color: var(--text-light);
+ opacity: 0.25;
+ cursor: not-allowed;
+ background: transparent;
+ text-decoration: line-through;
+}
+
+div.air-datepicker .air-datepicker-cell:not(.-disabled-):hover {
+ background: var(--primary-light);
+ color: var(--text-inverse);
cursor: pointer;
+ transform: scale(1.05);
+ transition: all var(--transition-fast);
+}
+
+div.air-datepicker .air-datepicker-cell.-selected- {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%) !important;
+ color: var(--text-inverse) !important;
+ font-weight: 700;
+ box-shadow: 0 2px 8px rgba(0, 115, 230, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ border: none !important;
+}
+
+div.air-datepicker .air-datepicker-cell.-in-range- {
+ background: linear-gradient(135deg, rgba(0, 115, 230, 0.2) 0%, rgba(0, 115, 230, 0.15) 100%) !important;
+ color: var(--primary-dark);
+ font-weight: 600;
+ border-top: 1px solid rgba(0, 115, 230, 0.3);
+ border-bottom: 1px solid rgba(0, 115, 230, 0.3);
+}
+
+div.air-datepicker .air-datepicker-cell.-range-from-,
+div.air-datepicker .air-datepicker-cell.-range-to- {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%) !important;
+ color: var(--text-inverse) !important;
+ font-weight: 700;
+ box-shadow: 0 2px 8px rgba(0, 115, 230, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ border: none !important;
+}
+
+/* Subtle indicator for dates with available data */
+div.air-datepicker .air-datepicker-cell.has-data {
+ font-weight: 600;
+ position: relative;
+ background: rgba(0, 115, 230, 0.06);
+ border-bottom: 2px solid rgba(0, 115, 230, 0.4);
+}
+
+div.air-datepicker .air-datepicker-cell.has-data:not(.-disabled-) {
+ color: var(--primary-dark);
+}
+
+/* When dates are selected, make them darker and more prominent */
+div.air-datepicker .air-datepicker-cell.-selected-.has-data,
+div.air-datepicker .air-datepicker-cell.-range-from-.has-data,
+div.air-datepicker .air-datepicker-cell.-range-to-.has-data {
+ background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%) !important;
+ color: var(--text-inverse) !important;
+ border-bottom: 2px solid var(--primary-dark);
+ font-weight: 700;
+ box-shadow: 0 3px 10px rgba(47, 56, 66, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
+}
+
+div.air-datepicker .air-datepicker-cell.-in-range-.has-data {
+ background: linear-gradient(135deg, rgba(0, 115, 230, 0.25) 0%, rgba(0, 115, 230, 0.2) 100%) !important;
+ color: var(--primary-dark);
+ border-bottom: 2px solid rgba(0, 115, 230, 0.5);
+ font-weight: 600;
+ border-top: 1px solid rgba(0, 115, 230, 0.4);
+}
+
+/* ============================================
+ Responsive Design
+ ============================================ */
+
+@media (max-width: 1200px) {
+ :root {
+ --sidebar-width: 360px;
+ }
+
+ .header-content {
+ padding: 0 1.5rem;
+ }
+
+ .site-title {
+ font-size: 1.25rem;
+ }
}
-/* Change layout for narrow viewport */
-@media (max-width: 800px) {
+@media (max-width: 968px) {
#viewer {
flex-direction: column;
}
#catalog-list {
+ flex: 0 0 auto;
width: 100%;
border-right: none;
+ border-bottom: 1px solid var(--border-color);
+ max-height: 60vh;
}
#details {
width: 100%;
}
+
+ .header-content {
+ padding: 0 1rem;
+ }
+
+ .site-title {
+ font-size: 1.1rem;
+ }
+
+ .sidebar-nav {
+ flex-direction: column;
+ }
}
-details h2 {
- font-size: medium;
+@media (max-width: 640px) {
+ :root {
+ --header-height: 60px;
+ }
+
+ .header-content {
+ padding: 0 1rem;
+ }
+
+ .site-title {
+ font-size: 1rem;
+ }
+
+ .logo-icon {
+ font-size: 1.5rem;
+ }
+
+ .sidebar-sticky {
+ padding: 1.5rem 1rem;
+ }
+
+ #details {
+ padding: 1.5rem 1rem;
+ }
+
+ .detail-section {
+ padding: 1.5rem;
+ }
+
+ .detail-section.collapsible summary {
+ padding: 1rem 1.5rem;
+ }
+
+ .detail-section.collapsible > *:not(summary) {
+ padding: 0 1.5rem 1.5rem 1.5rem;
+ }
+
+ .item {
+ padding: 1.25rem;
+ }
+
+ .item button.all {
+ right: 1rem;
+ top: 1rem;
+ width: 1.75rem;
+ height: 1.75rem;
+ font-size: 1rem;
+ }
}
-.has-data {
- background-color: rgba(160, 255, 160, 0.346);
+/* ============================================
+ Print Styles
+ ============================================ */
+
+@media print {
+ .main-header,
+ .sidebar-nav,
+ .sidebar-footer,
+ button {
+ display: none;
+ }
+
+ #viewer {
+ flex-direction: column;
+ }
+
+ #catalog-list,
+ #details {
+ width: 100%;
+ border: none;
+ box-shadow: none;
+ }
}
+/* ============================================
+ Geographic Region Selection
+ ============================================ */
-#date-picker {
- align-self: center;
- width: calc(50%);
- margin-top: 0.5em;
- margin-bottom: 0.5em;
+.region-selection {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: var(--bg-secondary);
+ border: 2px solid var(--border-light);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
}
-div.air-datepicker {
- align-self: center;;
+.region-title {
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 0 0 0.5rem 0;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.region-description {
+ margin: 0 0 1rem 0;
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ line-height: 1.5;
+}
+
+.region-controls {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.region-btn {
+ flex: 1;
+ min-width: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-sm);
+}
+
+.region-btn.primary {
+ background: var(--primary-color);
+ color: var(--text-inverse);
+}
+
+.region-btn.primary:hover {
+ background: var(--primary-dark);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-1px);
+}
+
+.region-btn.secondary {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border: 2px solid var(--border-color);
+}
+
+.region-btn.secondary:hover {
+ background: var(--bg-tertiary);
+ border-color: var(--text-secondary);
+}
+
+#map-container {
+ margin-top: 1.5rem;
+}
+
+.map-instructions {
+ margin-bottom: 1rem;
+ padding: 0.75rem 1rem;
+ background: var(--bg-primary);
+ border-left: 4px solid var(--primary-color);
+ border-radius: var(--radius-sm);
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+.map-instructions p {
+ margin: 0;
+ line-height: 1.5;
+}
+
+.map-instructions strong {
+ color: var(--text-primary);
+}
+
+#map {
+ border: 2px solid var(--border-light);
+ box-shadow: var(--shadow-md);
+}
+
+.selected-region {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+}
+
+.selected-region h4 {
+ margin: 0 0 1rem 0;
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.selected-region .code-block {
+ margin-bottom: 1rem;
+}
+
+.selected-region pre {
+ margin: 0;
+ padding: 1rem;
+ background: var(--bg-dark);
+ color: #e9ecef;
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+ line-height: 1.6;
+ overflow-x: auto;
+}
+
+/* ============================================
+ Animations
+ ============================================ */
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.item {
+ animation: fadeIn var(--transition-base) ease-out;
+}
+
+/* Loading State */
+.item.loading {
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ pointer-events: none;
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+/* Focus Visible for Accessibility */
+:focus-visible {
+ outline: 3px solid var(--primary-color);
+ outline-offset: 2px;
+}
+
+button:focus-visible,
+a:focus-visible {
+ outline: 3px solid var(--accent-color);
+}
+
+/* Canvas Elements */
+canvas {
+ width: 100%;
+ height: 300px;
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ margin-top: 1.5rem;
+}
+
+/* ============================================
+ Copy Button for Code Blocks
+ ============================================ */
+
+.code-block-with-copy {
+ position: relative;
+}
+
+.copy-btn {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: rgba(255, 255, 255, 0.9);
+ color: var(--text-primary);
+ border: 2px solid var(--border-color);
+ border-radius: var(--radius-sm);
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ z-index: 10;
+ box-shadow: var(--shadow-sm);
+}
+
+.copy-btn:hover {
+ background: var(--primary-color);
+ color: var(--text-inverse);
+ border-color: var(--primary-color);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.copy-btn.copied {
+ background: #28a745;
+ color: white;
+ border-color: #28a745;
+}
+
+.copy-btn svg {
+ flex-shrink: 0;
+}
+
+@media (max-width: 640px) {
+ .copy-btn {
+ padding: 0.4rem 0.75rem;
+ font-size: 0.8rem;
+ }
+
+ .copy-btn-text {
+ display: none;
+ }
+}
+
+/* ============================================
+ Polytope Query Section
+ ============================================ */
+
+.polytope-section {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: linear-gradient(135deg, #F6F9FC 0%, #F2F7FD 100%);
+ border: 2px solid var(--border-light);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+}
+
+.polytope-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 0 0 0.75rem 0;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.polytope-description {
+ margin: 0 0 1.25rem 0;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+.polytope-auth-form {
+ margin-bottom: 1.5rem;
+ padding: 1.25rem;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius-md);
+}
+
+.form-group {
+ margin-bottom: 1rem;
+}
+
+.form-group:last-child {
+ margin-bottom: 0;
+}
+
+.form-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+ color: var(--text-primary);
+ font-size: 0.9rem;
+ font-weight: 600;
+}
+
+.form-input {
+ width: 100%;
+ padding: 0.75rem;
+ border: 2px solid var(--border-light);
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+ font-family: inherit;
+ color: var(--text-primary);
+ background: var(--bg-primary);
+ transition: all var(--transition-base);
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(0, 115, 230, 0.1);
+}
+
+.form-input::placeholder {
+ color: var(--text-light);
+}
+
+.form-hint {
+ margin: 0.5rem 0 0 0;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ line-height: 1.4;
+}
+
+.form-hint a {
+ color: var(--primary-color);
+ text-decoration: none;
+}
+
+.form-hint a:hover {
+ text-decoration: underline;
+}
+
+.polytope-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
+ color: var(--text-inverse);
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-md);
+}
+
+.polytope-btn:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-lg);
+}
+
+.polytope-btn:active {
+ transform: translateY(0);
+}
+
+.polytope-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.polytope-status {
+ margin-top: 1.25rem;
+ padding: 1rem;
+ background: var(--bg-primary);
+ border-left: 4px solid var(--primary-color);
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+ color: var(--text-primary);
+}
+
+.polytope-status.loading {
+ border-left-color: var(--secondary-color);
+}
+
+.polytope-status.success {
+ border-left-color: #28a745;
+}
+
+.polytope-status.error {
+ border-left-color: var(--accent-color);
+ color: #dc3545;
+}
+
+.polytope-results {
+ margin-top: 1.25rem;
+}
+
+.polytope-result-item {
+ padding: 1rem;
+ margin-bottom: 0.75rem;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius-md);
+}
+
+.polytope-result-item.success {
+ border-left: 4px solid #28a745;
+}
+
+.polytope-result-item.error {
+ border-left: 4px solid #dc3545;
+}
+
+.polytope-result-header {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ color: var(--text-primary);
+ font-size: 0.9rem;
+}
+
+.polytope-result-detail {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ font-family: 'Monaco', 'Courier New', monospace;
+}
+.download-json-btn {
+ transition: all 0.2s ease;
+}
+
+.download-json-btn:hover {
+ background: var(--secondary-color) !important;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 115, 230, 0.3);
+}
+
+.download-json-btn:active {
+ transform: translateY(0);
}
diff --git a/stac_server/templates/index.html b/stac_server/templates/index.html
index c14e927c..8aeb9a86 100644
--- a/stac_server/templates/index.html
+++ b/stac_server/templates/index.html
@@ -7,7 +7,7 @@
{{title}}
-
+
@@ -16,6 +16,15 @@
+
+
+
+
+
+
+
+
+
-->
-
-
- Currently Selected Tree
-
- This shows the data qube that
- matches
- the current query. The leaves are the next set of available selections you can make.
-
-
-
+
+
+
+
+
+
+
+
+
+ Selected MARS Requests
+
+
+ These are the complete MARS requests matching your selection criteria. You can now use these to retrieve data.
+
+
+
+
+
+
+
+
+
+ Query Data with Polytope
+
+
Extract the actual data for these MARS requests using the Destination Earth Polytope data extraction service. You'll need to provide your Destination Earth credentials.
+
+
+
+
+
+
+
+
+ Query Polytope Service
+
+
+
+
+
+
+
+
+
+
+
+ Currently Selected Tree
+
+
+ This shows the data qube that matches the current query. The leaves are the next set of available selections you can make.
+
+
+
+
+
- Example Qubed Code
+
+
+
+
+ Example Qubed Code
+
- See the Qubed documentation for more details.
-
-# pip install qubed requests
+
+ See the Qubed documentation for more details.
+
+
+
# pip install qubed requests
import requests
from qubed import Qube
qube = Qube.from_json(requests.get("{{ api_url }}select/?{{request.url.query}}").json())
-qube.print()
-
+qube.print()
+
-
-
+
- Raw STAC Response
+
+
+
+
+ Raw STAC Response
+
- See the STAC
- Extension
- Proposal for more details on the format.
-
+
+ See the STAC Extension Proposal for more details on the format.
+
+
-
-
+
- Debug Info
+
+
+
+
+ Debug Info
+
-
+
-
+
+