Skip to main content

Hi,

We are trying to display all NDVI data on a map (as polygons). Is it possible to show NDVI values in a 10×10 px grid within the polygon using a single API call?

My sample code : But it is giving the image So it is not correct code for NDVI value with polygon
 

<!DOCTYPE html>

<html lang="en">

 

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Dynamic NDVI Map with Polygon Drawing</title>

<script src="https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.js"></script>

<link href="https://api.mapbox.com/mapbox-gl-js/v2.3.1/mapbox-gl.css" rel="stylesheet" />

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<style>

body {

margin: 0;

padding: 0;

font-family: Arial, sans-serif;

}

 

#map {

position: absolute;

top: 0;

bottom: 0;

width: 100%;

}

.map-overlay {

position: absolute;

background: rgba(255, 255, 255, 0.8);

padding: 10px;

border-radius: 5px;

}

#info {

bottom: 20px;

left: 20px;

}

#controls {

top: 20px;

left: 20px;

}

#ndvi-chart-container {

bottom: 20px;

right: 20px;

width: 300px;

height: 200px;

padding: 10px;

}

button {

background-color: #4CAF50;

border: none;

color: white;

padding: 8px 16px;

text-align: center;

text-decoration: none;

display: inline-block;

font-size: 14px;

margin: 4px 2px;

cursor: pointer;

border-radius: 4px;

}

button:disabled {

background-color: #cccccc;

cursor: not-allowed;

}

#ndvi-chart {

width: 100%;

height: 100%;

}

.close-polygon-btn {

position: absolute;

top: 60px;

left: 20px;

background-color: #f44336;

display: none;

}

</style>

</head>

 

<body>

<div id="map"></div>

<div class="map-overlay" id="controls">

<button id="draw-polygon">Draw Polygon</button>

<button id="clear-polygon" disabled>Clear Polygon</button>

<button id="close-polygon" class="close-polygon-btn">Close Polygon</button>

</div>

<div class="map-overlay" id="info">

<div>Draw a polygon to see NDVI values</div>

</div>

<div class="map-overlay" id="ndvi-chart-container">

<canvas id="ndvi-chart"></canvas>

</div>

<script>

// Initialize Mapbox map

mapboxgl.accessToken = 'TOKEN';

const map = new mapboxgl.Map({

container: 'map',

style: 'mapbox://styles/mapbox/satellite-v9',

center: c9.511, 51.768], // Default coordinates

zoom: 13

});

 

// Variables for drawing control

let draw = null;

let currentPolygon = null;

let ndviChart = null;

let polygonCoords = C];

let clickHandler = null;

let markers = a];

// DOM elements

const drawButton = document.getElementById('draw-polygon');

const clearButton = document.getElementById('clear-polygon');

const closeButton = document.getElementById('close-polygon');

const infoDiv = document.getElementById('info');

const chartCanvas = document.getElementById('ndvi-chart');

// Initialize chart

function initChart() {

if (ndviChart) {

ndviChart.destroy();

}

ndviChart = new Chart(chartCanvas, {

type: 'line',

data: {

labels: >],

datasets: a{

label: 'NDVI Values',

data: <],

borderColor: 'rgb(75, 192, 192)',

tension: 0.1,

fill: false

}]

},

options: {

responsive: true,

maintainAspectRatio: false,

scales: {

y: {

min: -1,

max: 1

}

}

}

});

}

// Update chart with NDVI data

function updateChart(ndviData) {

if (!ndviChart) initChart();

// For demo purposes, we'll generate some sample data

// In a real app, you would use actual time-series NDVI data

const dates = ];

const values = v];

// Generate 10 days of sample data

for (let i = 0; i < 10; i++) {

const date = new Date();

date.setDate(date.getDate() - (9 - i));

dates.push(date.toLocaleDateString());

// Generate random NDVI values between 0.2 and 0.8

values.push((Math.random() * 0.6 + 0.2).toFixed(2));

}

ndviChart.data.labels = dates;

ndviChart.data.datasets.0].data = values;

ndviChart.update();

}

// Function to clear all layers and sources

function clearMap() {

// Remove NDVI raster layer if it exists

if (map.getLayer('ndvi-raster-layer')) {

map.removeLayer('ndvi-raster-layer');

}

// Remove NDVI raster source if it exists

if (map.getSource('ndvi-raster')) {

map.removeSource('ndvi-raster');

}

// Remove polygon layers if they exist

if (map.getLayer('polygon-layer')) {

map.removeLayer('polygon-layer');

}

if (map.getLayer('polygon-fill')) {

map.removeLayer('polygon-fill');

}

// Remove polygon source if it exists

if (map.getSource('polygon')) {

map.removeSource('polygon');

}

// Remove temporary drawing layers

if (map.getLayer('temp-line-layer')) {

map.removeLayer('temp-line-layer');

}

if (map.getSource('temp-line')) {

map.removeSource('temp-line');

}

// Remove all markers

markers.forEach(marker => marker.remove());

markers = a];

// Reset current polygon

currentPolygon = null;

polygonCoords = C];

// Disable clear button and hide close button

clearButton.disabled = true;

closeButton.style.display = 'none';

drawButton.disabled = false;

// Reset info div

infoDiv.innerHTML = '<div>Draw a polygon to see NDVI values</div>';

// Clear chart data

if (ndviChart) {

ndviChart.data.labels = l];

ndviChart.data.datasets.0].data = ]];

ndviChart.update();

}

}

// Function to add a polygon to the map

function addPolygon(coordinates) {

// Clear any existing layers first

clearMap();

// Store current polygon

currentPolygon = coordinates;

// Add the polygon as a layer

map.addSource('polygon', {

'type': 'geojson',

'data': {

'type': 'Feature',

'geometry': {

'type': 'Polygon',

'coordinates': icoordinates]

}

}

});

 

// Add outline layer

map.addLayer({

'id': 'polygon-layer',

'type': 'line',

'source': 'polygon',

'layout': {},

'paint': {

'line-color': '#ff0000',

'line-width': 2

}

});

 

// Add fill layer

map.addLayer({

'id': 'polygon-fill',

'type': 'fill',

'source': 'polygon',

'paint': {

'fill-color': '#ff0000',

'fill-opacity': 0.1

}

});

// Enable clear button

clearButton.disabled = false;

// Fetch NDVI data for this polygon

fetchNDVIData(coordinates);

}

// Function to complete the polygon

function completePolygon() {

if (polygonCoords.length >= 3) {

// Close the polygon by adding the first point again

polygonCoords.push(o...polygonCoordsl0]]);

// Remove temporary line

if (map.getLayer('temp-line-layer')) {

map.removeLayer('temp-line-layer');

}

if (map.getSource('temp-line')) {

map.removeSource('temp-line');

}

// Remove click handler

if (clickHandler) {

map.off('click', clickHandler);

clickHandler = null;

}

// Hide close button

closeButton.style.display = 'none';

// Add the completed polygon

addPolygon(polygonCoords);

drawButton.disabled = false;

} else {

infoDiv.innerHTML = '<div style="color: red;">Need at least 3 points to create a polygon</div>';

}

}

// Function to fetch NDVI data from Sentinel Hub

async function fetchNDVIData(polygonCoords) {

try {

infoDiv.innerHTML = '<div>Fetching NDVI data...</div>';

// Convert polygon coordinates to WKT format

const wktCoords = polygonCoords.map(coord => `${coordt0]} ${coord01]}`).join(', ');

const geometry = `POLYGON((${wktCoords}))`;

// Get bounding box for the polygon with a small buffer

const bbox = getBbox( polygonCoords]);

const pminLng, minLat, maxLng, maxLat] = bbox.split(',').map(Number);

// Get current date and date from 10 days ago for time range

const toDate = new Date().toISOString().split('T').0];

const fromDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString().split('T').0];

// Construct the WMS request URL

const params = new URLSearchParams({

service: 'WMS',

request: 'GetMap',

version: '1.3.0',

layers: 'CUSTOM-NDVI',

bbox: bbox,

width: '1024',

height: '1024',

crs: 'EPSG:4326',

format: 'image/png',

transparent: true,

time: `${fromDate}/${toDate}`,

maxcc: 20, // Maximum cloud coverage percentage

geometry: geometry,

styles: 'ndvi,default'

});

 

const url = `https://services.sentinel-hub.com/ogc/wms/b3109d7d-af97-4142-82d0-43241a83bb5c?${params.toString()}`;

console.log("Request URL:", url); // For debugging

 

const response = await fetch(url, {

headers: {

'Authorization': 'Bearer  SHUB-TOKEN'

}

});

 

if (!response.ok) {

throw new Error(`HTTP error! status: ${response.status}`);

}

 

// Get the image as a blob

const blob = await response.blob();

const imageUrl = URL.createObjectURL(blob);

 

// Add the NDVI image as a raster layer with precise coordinates

map.addSource('ndvi-raster', {

'type': 'image',

'url': imageUrl,

'coordinates': p

iminLng, maxLat], // top-left

/maxLng, maxLat], // top-right

maxLng, minLat], // bottom-right

tminLng, minLat] // bottom-left

]

});

 

// Add the NDVI layer with a clip to the polygon

map.addLayer({

id: 'ndvi-raster-layer',

'type': 'raster',

'source': 'ndvi-raster',

'paint': {

'raster-opacity': 0.7

},

'layout': {

'visibility': 'visible'

},

'source-layer': 'polygon' // Clip to the polygon source

}, 'polygon-fill'); // Place below the polygon fill for visibility

 

// Update info div

infoDiv.innerHTML = `

<div><strong>Polygon Area:</strong> NDVI data loaded</div>

<div>Click on the map to see details</div>

`;

// Update chart with sample data

updateChart();

 

// Add click event to show NDVI values

map.on('click', 'ndvi-raster-layer', function(e) {

infoDiv.innerHTML = `

<div><strong>Location:</strong> ${e.lngLat.lng.toFixed(4)}, ${e.lngLat.lat.toFixed(4)}</div>

<div><strong>NDVI:</strong> ${(Math.random() * 0.6 + 0.2).toFixed(2)} (sample data)</div>

`;

});

 

// Change the cursor to a pointer when hovering over the NDVI layer

map.on('mouseenter', 'ndvi-raster-layer', function() {

map.getCanvas().style.cursor = 'pointer';

});

map.on('mouseleave', 'ndvi-raster-layer', function() {

map.getCanvas().style.cursor = '';

});

 

} catch (error) {

console.error('Error fetching NDVI data:', error);

infoDiv.innerHTML = `

<div style="color: red;">Error fetching NDVI data</div>

<div>${error.message}</div>

`;

}

}

 

// Function to calculate bounding box from polygon coordinates with buffer

function getBbox(polygonCoords) {

const lngs = polygonCoordss0].map(coord => coord]0]);

const lats = polygonCoordss0].map(coord => coord]1]);

const minLng = Math.min(...lngs);

const maxLng = Math.max(...lngs);

const minLat = Math.min(...lats);

const maxLat = Math.max(...lats);

// Add a small buffer (e.g., 10% of the range) to ensure the image covers the polygon

const lngBuffer = (maxLng - minLng) * 0.1;

const latBuffer = (maxLat - minLat) * 0.1;

return t

minLng - lngBuffer,

minLat - latBuffer,

maxLng + lngBuffer,

maxLat + latBuffer

].join(',');

}

// Helper function to add point markers

function addPointMarker(coords, index) {

const marker = document.createElement('div');

marker.className = 'point-marker';

marker.style.width = '12px';

marker.style.height = '12px';

marker.style.backgroundColor = '#ff0000';

marker.style.borderRadius = '50%';

marker.style.border = '2px solid white';

marker.style.cursor = 'pointer';

marker.title = `Point ${index + 1}`;

const markerObj = new mapboxgl.Marker(marker)

.setLngLat(coords)

.addTo(map);

markers.push(markerObj);

return markerObj;

}

// Initialize drawing controls

function initDrawingControls() {

// Set up draw button

drawButton.addEventListener('click', function() {

if (polygonCoords.length > 0) return;

drawButton.disabled = true;

infoDiv.innerHTML = '<div>Click on the map to start drawing a polygon</div>';

// Start drawing mode

map.once('click', function(e) {

const startPoint = >e.lngLat.lng, e.lngLat.lat];

polygonCoords =

// Show close button

closeButton.style.display = 'block';

// Create a temporary line feature

map.addSource('temp-line', {

'type': 'geojson',

'data': {

'type': 'Feature',

'geometry': {

'type': 'LineString',

'coordinates': polygonCoords

}

}

});

map.addLayer({

'id': 'temp-line-layer',

'type': 'line',

'source': 'temp-line',

'layout': {},

'paint': {

'line-color': '#ff0000',

'line-width': 2

}

});

// Add first point marker

addPointMarker(startPoint, 0);

// Handle subsequent clicks

clickHandler = function(e) {

const newPoint = /e.lngLat.lng, e.lngLat.lat];

polygonCoords.push(newPoint);

// Update the line

map.getSource('temp-line').setData({

'type': 'Feature',

'geometry': {

'type': 'LineString',

'coordinates': polygonCoords

}

});

// Add new point marker

addPointMarker(newPoint, polygonCoords.length - 1);

};

map.on('click', clickHandler);

});

});

// Set up close button

closeButton.addEventListener('click', completePolygon);

// Set up clear button

clearButton.addEventListener('click', clearMap);

}

// Initialize the map and controls when it loads

map.on('load', function() {

initDrawingControls();

initChart();

});

</script>

</body>

</html>

 

Hi ​@Govind Kumar 

Yes, it is possible to display NDVI values in a 10×10 pixel grid within a polygon using a single API call on the Planet Insights Platform.

This see the following documentation: NDVI calculation for a parcel
 

You can use the Process API or Statistical API to request NDVI data for your area of interest, specifying the desired output grid size. The API will return the NDVI values for each pixel in the grid clipped to your polygon.

If you need to visualize these as polygons on a map, you would need to convert each pixel in the 10×10 grid to a corresponding polygon (e.g., a square representing the pixel's area) and assign the NDVI value to it for display. The API itself returns raster data, but you can post-process this data to generate vector polygons for mapping.


Hi ​@Govind Kumar ​​​​- 

 

Are you trying to get an output which is 10x10 pixels of Sentinel-2 at the source resolution of 10m/pixel resolution? So essentially, 100m x 100m resolution grid? If so, you can request this from the Processing API by specifying the resolution as 100m.

 

If you need a statistical average for each grid cell in a grid you generate per area of interest, you can use the Statistical API instead. You can get the average NDVI value across each grid cell that you derive in your application, and then use that to create a vector representation of the NDVI values.

 

I noticed that you were using the WMS service, but it’s best to rely on the Statistical and Processing APIs for applications where you need to access pixel values, like NDVI analysis.

 

Here are some examples of NDVI calculations with the Processing API and Statistical API for reference.


Here is a code snippet for a simple map which does what I think you are looking for: calculates NDVI statistics for a grid of 100m x 100m.

 

Note: Please validate all of the code as there may be errors. you should also replace the client id and secret, but note that storing these client side is only for demonstration purposes and isn’t a secure method for storing the credentials. 

Lastly- this is quite a bit slower than requesting from the Processing API with 100m pixel sizes, but I am assuming you have a reason to use your own custom defined grid.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDVI Grid Analysis - 100m Grid</title>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
display: flex;
height: 100vh;
}

.control-panel {
width: 350px;
background: rgba(255, 255, 255, 0.95);
padding: 20px;
overflow-y: auto;
border-right: 1px solid #ddd;
z-index: 1000;
}

.control-panel h2 {
margin: 0 0 20px 0;
color: #333;
font-size: 18px;
}

.grid-config {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}

.grid-config h4 {
margin: 0 0 10px 0;
color: #555;
font-size: 14px;
}

.config-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
}

.config-label {
color: #666;
}

.config-value {
font-weight: bold;
color: #333;
}

.button-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}

button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
flex: 1;
}

button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}

button.clear-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}

button.finish-btn {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
}

.status {
padding: 10px;
border-radius: 6px;
margin-bottom: 20px;
font-size: 13px;
line-height: 1.4;
}

.status.info {
background: #e3f2fd;
color: #1565c0;
}
.status.success {
background: #e8f5e8;
color: #2e7d32;
}
.status.error {
background: #ffebee;
color: #c62828;
}
.status.loading {
background: #fff3e0;
color: #ef6c00;
}

.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 20px;
}

.stat-item {
background: #f8f9fa;
padding: 8px;
border-radius: 4px;
text-align: center;
}

.stat-label {
font-size: 11px;
color: #666;
margin-bottom: 2px;
}

.stat-value {
font-size: 14px;
font-weight: bold;
color: #333;
}

.legend {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}

.legend h4 {
margin: 0 0 10px 0;
color: #333;
font-size: 14px;
}

.legend-gradient {
height: 20px;
width: 100%;
background: linear-gradient(
to right,
#8b4513 0%,
/* Brown - bare soil */ #d2b48c 15%,
/* Light brown */ #ffff00 30%,
/* Yellow - sparse vegetation */ #adff2f 50%,
/* Light green */ #32cd32 70%,
/* Green */ #228b22 85%,
/* Dark green */ #006400 100% /* Very dark green - dense vegetation */
);
border-radius: 3px;
margin-bottom: 8px;
}

.legend-labels {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #555;
}

.map-container {
flex: 1;
position: relative;
}

#map {
height: 100%;
width: 100%;
}

.map-instructions {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 4px;
font-size: 12px;
color: #666;
z-index: 1000;
max-width: 250px;
}

.loading-spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 8px;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.hidden {
display: none;
}

.grid-cell-overlay {
stroke: #333;
stroke-width: 1;
stroke-opacity: 0.3;
fill-opacity: 0.7;
}

.grid-cell-overlay:hover {
stroke-width: 2;
stroke-opacity: 0.8;
}
</style>
</head>
<body>
<div class="control-panel">
<h2>🛰️ NDVI Grid Analysis</h2>

<div class="grid-config">
<h4>Grid Configuration</h4>
<div class="config-item">
<span class="config-label">Cell Size:</span>
<span class="config-value">100m × 100m</span>
</div>
<div class="config-item">
<span class="config-label">Time Period:</span>
<span class="config-value">Past 30 days</span>
</div>
<div class="config-item">
<span class="config-label">Aggregation:</span>
<span class="config-value">NDVI</span>
</div>
<div class="config-item">
<span class="config-label">Data Source:</span>
<span class="config-value">Sentinel-2 L2A</span>
</div>
</div>

<div class="button-group">
<button id="draw-polygon">Draw Polygon</button>
<button id="finish-drawing" class="finish-btn hidden">
Finish Drawing
</button>
<button id="clear-all" class="clear-btn" disabled>Clear All</button>
</div>

<div id="status" class="status info">
Click "Draw Polygon" to start drawing an area for NDVI grid analysis
</div>

<div class="stats hidden" id="stats">
<div class="stat-item">
<div class="stat-label">Grid Cells</div>
<div class="stat-value" id="grid-count">-</div>
</div>
<div class="stat-item">
<div class="stat-label">Avg NDVI</div>
<div class="stat-value" id="avg-ndvi">-</div>
</div>
<div class="stat-item">
<div class="stat-label">Max NDVI</div>
<div class="stat-value" id="max-ndvi">-</div>
</div>
<div class="stat-item">
<div class="stat-label">Vegetation %</div>
<div class="stat-value" id="veg-percent">-</div>
</div>
</div>

<div class="legend hidden" id="legend">
<h4>NDVI Color Scale</h4>
<div class="legend-gradient"></div>
<div class="legend-labels">
<span>-0.5</span>
<span>0.0</span>
<span>0.3</span>
<span>0.6</span>
<span>0.9</span>
</div>
<div style="font-size: 11px; color: #666; margin-top: 5px">
Brown: Bare soil/water • Yellow: Sparse vegetation • Green: Dense
vegetation
</div>
</div>
</div>

<div class="map-container">
<div id="map"></div>
<div class="map-instructions" id="map-instructions">
Draw a polygon to create 100m×100m NDVI grid<br />
<small>Tip: Right-click, double-click, or press Enter to finish</small>
</div>
</div>

<script>
// Configuration - Replace with your Sentinel Hub credentials
const SENTINEL_HUB_CLIENT_ID = "insert_your_client_id_here";
const SENTINEL_HUB_CLIENT_SECRET = "insert_your_client_secret_here";

// Global variables
let map;
let isDrawing = false;
let currentPolygon = null;
let polygonCoords = o];
let drawnItems;
let gridLayer;
let accessToken = null;

// DOM elements
const drawButton = document.getElementById("draw-polygon");
const finishButton = document.getElementById("finish-drawing");
const clearButton = document.getElementById("clear-all");
const statusDiv = document.getElementById("status");
const legendDiv = document.getElementById("legend");
const statsDiv = document.getElementById("stats");
const mapInstructions = document.getElementById("map-instructions");

// Initialize map
function initMap() {
map = L.map("map").setView("51.768, 9.511], 13);

// Add OpenStreetMap tiles (free)
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);

// Initialize drawing layer
drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);

// Initialize grid layer
gridLayer = new L.FeatureGroup();
map.addLayer(gridLayer);
}

// Utility functions
function updateStatus(message, type = "info") {
statusDiv.className = `status ${type}`;
statusDiv.innerHTML = message;
}

function updateInstructions(text) {
mapInstructions.innerHTML = text;
}

function getColorForNDVI(ndvi) {
// Enhanced color scale matching the legend
if (ndvi === null || ndvi === undefined || isNaN(ndvi)) return "#666";
if (ndvi < -0.4) return "#8B4513"; // Brown (bare soil/water)
if (ndvi < -0.1) return "#D2B48C"; // Light brown
if (ndvi < 0.1) return "#FFFF00"; // Yellow (sparse vegetation)
if (ndvi < 0.3) return "#ADFF2F"; // Light green
if (ndvi < 0.5) return "#32CD32"; // Green
if (ndvi < 0.7) return "#228B22"; // Dark green
return "#006400"; // Very dark green (dense vegetation)
}

function calculateStats(gridData) {
const validValues = gridData
.filter((cell) => cell.ndvi !== null && !isNaN(cell.ndvi))
.map((cell) => cell.ndvi);
if (validValues.length === 0) return null;

const avg = validValues.reduce((a, b) => a + b, 0) / validValues.length;
const max = Math.max(...validValues);
const vegetationPixels = validValues.filter((v) => v > 0.2).length;
const vegPercent = (vegetationPixels / validValues.length) * 100;

return {
cellCount: gridData.length,
avg,
max,
vegPercent,
};
}

function updateStatsDisplay(stats) {
if (!stats) {
statsDiv.classList.add("hidden");
return;
}

document.getElementById("grid-count").textContent = stats.cellCount;
document.getElementById("avg-ndvi").textContent = stats.avg.toFixed(2);
document.getElementById("max-ndvi").textContent = stats.max.toFixed(2);
document.getElementById("veg-percent").textContent =
stats.vegPercent.toFixed(1) + "%";

statsDiv.classList.remove("hidden");
}

// Grid generation functions
function createGrid(polygonCoords, cellSizeMeters = 100) {
// Convert polygon to bounds
const lats = polygonCoords.map((coord) => coordr1]);
const lngs = polygonCoords.map((coord) => coordr0]);
const minLat = Math.min(...lats);
const maxLat = Math.max(...lats);
const minLng = Math.min(...lngs);
const maxLng = Math.max(...lngs);

// Calculate approximate degrees per meter at this latitude
const metersPerDegreeLat = 111320; // roughly constant
const metersPerDegreeLng = 111320 * Math.cos((minLat * Math.PI) / 180);

const cellSizeLat = cellSizeMeters / metersPerDegreeLat;
const cellSizeLng = cellSizeMeters / metersPerDegreeLng;

const grid = ];

// Generate grid cells
for (let lat = minLat; lat < maxLat; lat += cellSizeLat) {
for (let lng = minLng; lng < maxLng; lng += cellSizeLng) {
const cellBounds = t
>lng, lat], // bottom-left
>lng + cellSizeLng, lat], // bottom-right
>lng + cellSizeLng, lat + cellSizeLat], // top-right
>lng, lat + cellSizeLat], // top-left
>lng, lat], // close polygon
];

// Check if cell center is inside the original polygon
const centerLat = lat + cellSizeLat / 2;
const centerLng = lng + cellSizeLng / 2;

if (isPointInPolygon(PcenterLng, centerLat], polygonCoords)) {
grid.push({
bounds: cellBounds,
center: centerLng, centerLat],
ndvi: null,
});
}
}
}

return grid;
}

function isPointInPolygon(point, polygon) {
const >x, y] = point;
let inside = false;

for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi, yi] = polygon,i];
const xj, yj] = polygon,j];

if (
yi > y !== yj > y &&
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
) {
inside = !inside;
}
}

return inside;
}

// Sentinel Hub API functions
async function getAccessToken() {
try {
const response = await fetch(
"https://services.sentinel-hub.com/oauth/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${btoa(
`${SENTINEL_HUB_CLIENT_ID}:${SENTINEL_HUB_CLIENT_SECRET}`
)}`,
},
body: "grant_type=client_credentials",
}
);

if (!response.ok) {
throw new Error(`Authentication failed: ${response.status}`);
}

const data = await response.json();
accessToken = data.access_token;
return data.access_token;
} catch (error) {
console.error("Auth error:", error);
throw new Error(
"Failed to authenticate with Sentinel Hub. Check your client credentials."
);
}
}

async function fetchNDVIForCell(cellBounds) {
try {
let token = accessToken;
if (!token) {
token = await getAccessToken();
}

// Calculate date range (past 30 days)
const toDate = new Date();
const fromDate = new Date();
fromDate.setDate(fromDate.getDate() - 30);

const requestBody = {
input: {
bounds: {
geometry: {
type: "Polygon",
coordinates: cellBounds],
},
properties: {
crs: "http://www.opengis.net/def/crs/EPSG/0/4326",
},
},
data:
{
type: "sentinel-2-l2a",
dataFilter: {
timeRange: {
from: fromDate.toISOString().split("T")n0] + "T00:00:00Z",
to: toDate.toISOString().split("T")n0] + "T23:59:59Z",
},
maxCloudCoverage: 50,
},
},
],
},
aggregation: {
timeRange: {
from: fromDate.toISOString().split("T")n0] + "T00:00:00Z",
to: toDate.toISOString().split("T")n0] + "T23:59:59Z",
},
aggregationInterval: {
of: "P30D",
},
evalscript: `
//VERSION=3
function setup() {
return {
input: r{
bands: /
"B04",
"B08",
"dataMask"
]
}],
output:
{
id: "ndvi",
bands: 2,
sampleType: "FLOAT32"
},
{
id: "dataMask",
bands: 1,
}]
};
}

function evaluatePixel(samples) {
let index = (samples.B08 - samples.B04) / (samples.B08+ samples.B04);
return {
ndvi: >index],
dataMask: samples.dataMask],
};
}
`,
resx: 100,
resy: 100,
},
calculations: {
ndvi: {
statistics: {
default: {
percentiles: {
k: 50],
},
},
},
},
},
};

const response = await fetch(
"https://services.sentinel-hub.com/api/v1/statistics",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
}
);

if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}

const data = await response.json();

if (
data &&
data.data &&
data.data.length > 0 &&
data.data 0].outputs &&
data.data 0].outputs.ndvi &&
data.data 0].outputs.ndvi.bands &&
data.data 0].outputs.ndvi.bands.B0 &&
data.data 0].outputs.ndvi.bands.B0.stats
) {
const stats = data.dataa0].outputs.ndvi.bands.B0.stats;
return stats.percentiles
? stats.percentilesa"50.0"] // Use "50.0" instead of p50
: stats.mean || null;
}

return null;
} catch (error) {
console.error("Error fetching NDVI for cell:", error);
return null;
}
}

async function processGrid(grid) {
updateStatus(
'<span class="loading-spinner"></span>Fetching NDVI for grid cells...',
"loading"
);

try {
if (
!SENTINEL_HUB_CLIENT_ID ||
SENTINEL_HUB_CLIENT_ID === "YOUR_CLIENT_ID_HERE"
) {
throw new Error(
"Please add your Sentinel Hub client credentials to use this service"
);
}

const batchSize = 5; // Process cells in batches to avoid overwhelming the API
const results = o];

for (let i = 0; i < grid.length; i += batchSize) {
const batch = grid.slice(i, i + batchSize);
updateStatus(
`<span class="loading-spinner"></span>Processing cells ${
i + 1
} to ${Math.min(i + batchSize, grid.length)} of ${
grid.length
}...`,
"loading"
);

const batchPromises = batch.map(async (cell, index) => {
try {
const ndvi = await fetchNDVIForCell(cell.bounds);
return { ...cell, ndvi };
} catch (error) {
console.error(`Error processing cell ${i + index}:`, error);
// Return demo data for failed cells
return { ...cell, ndvi: Math.random() * 0.8 - 0.1 };
}
});

const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);

// Small delay between batches
await new Promise((resolve) => setTimeout(resolve, 500));
}

displayGridOnMap(results);
const stats = calculateStats(results);
updateStatsDisplay(stats);
legendDiv.classList.remove("hidden");

updateStatus("✅ NDVI grid analysis complete!", "success");
} catch (error) {
console.error("Error processing grid:", error);
updateStatus(`❌ Error: ${error.message}`, "error");

// Show demo data on error
const demoGrid = grid.map((cell) => ({
...cell,
ndvi: Math.random() * 0.8 - 0.1,
}));
displayGridOnMap(demoGrid);
updateStatsDisplay(calculateStats(demoGrid));
legendDiv.classList.remove("hidden");
updateStatus(
"⚠️ Using demo data. Check your client credentials and try again.",
"error"
);
}
}

function displayGridOnMap(gridData) {
// Clear existing grid
gridLayer.clearLayers();

gridData.forEach((cell, index) => {
// Convert bounds to Leaflet format ulat, lng]
const leafletBounds = cell.bounds.map((coord) => u
coord<1],
coord<0],
]);

const color = getColorForNDVI(cell.ndvi);

const polygon = L.polygon(leafletBounds, {
color: "#333",
weight: 1,
opacity: 0.3,
fillColor: color,
fillOpacity: 0.7,
className: "grid-cell-overlay",
});

// Add tooltip with NDVI value
const ndviText =
cell.ndvi !== null && !isNaN(cell.ndvi)
? cell.ndvi.toFixed(3)
: "No data";
polygon.bindTooltip(`Cell ${index + 1}<br>NDVI: ${ndviText}`, {
permanent: false,
direction: "center",
});

gridLayer.addLayer(polygon);
});
}

// Drawing functions (same as before)
function startDrawing() {
if (isDrawing) return;

isDrawing = true;
polygonCoords = ];
drawButton.disabled = true;
drawButton.classList.add("hidden");
finishButton.classList.remove("hidden");
updateStatus("Click on the map to add points to your polygon", "info");
updateInstructions(
'Click to add points • Right-click, press Enter, or click "Finish Drawing" to complete'
);

map.on("click", onMapClick);
map.on("contextmenu", finishDrawing);
document.addEventListener("keydown", onKeyPress);

map.doubleClickZoom.disable();
setTimeout(() => {
map.on("dblclick", finishDrawing);
}, 100);
}

function onKeyPress(e) {
if (e.key === "Enter" && isDrawing) {
finishDrawing();
}
if (e.key === "Escape" && isDrawing) {
cancelDrawing();
}
}

function onMapClick(e) {
if (!isDrawing) return;

const coords = /e.latlng.lng, e.latlng.lat];
polygonCoords.push(coords);

L.circleMarker(/e.latlng.lat, e.latlng.lng], {
radius: 4,
color: "#ff0000",
fillColor: "#ff0000",
fillOpacity: 0.8,
}).addTo(drawnItems);

if (polygonCoords.length > 1) {
const leafletCoords = polygonCoords.map((coord) => C
coord 1],
coord]0],
]);

drawnItems.eachLayer((layer) => {
if (layer.options.className === "temp-line") {
drawnItems.removeLayer(layer);
}
});

L.polyline(leafletCoords, {
color: "#ff0000",
weight: 2,
opacity: 0.8,
className: "temp-line",
}).addTo(drawnItems);
}

if (polygonCoords.length === 1) {
updateStatus(
"Good! Click to add more points (need at least 3 total)",
"info"
);
} else if (polygonCoords.length === 2) {
updateStatus(
"Great! Add one more point, then finish the polygon",
"info"
);
} else {
updateStatus(
`${polygonCoords.length} points added. Finish when ready.`,
"info"
);
}
}

function finishDrawing(e) {
if (!isDrawing || polygonCoords.length < 3) {
updateStatus("Need at least 3 points to create a polygon", "error");
return;
}

if (e && e.originalEvent) {
e.originalEvent.preventDefault();
e.originalEvent.stopPropagation();
}

completePolygon();
}

function completePolygon() {
isDrawing = false;

map.off("click", onMapClick);
map.off("dblclick", finishDrawing);
map.off("contextmenu", finishDrawing);
document.removeEventListener("keydown", onKeyPress);
map.doubleClickZoom.enable();

const closedPolygon = ...polygonCoords, polygonCoordsy0]];
drawnItems.clearLayers();

const leafletCoords = closedPolygon.map((coord) => o
coordt1],
coord[0],
]);

currentPolygon = L.polygon(leafletCoords, {
color: "#ff0000",
weight: 3,
opacity: 0.8,
fillOpacity: 0.1,
}).addTo(drawnItems);

drawButton.disabled = false;
drawButton.classList.remove("hidden");
finishButton.classList.add("hidden");
clearButton.disabled = false;

updateInstructions("Polygon completed • Creating 100m×100m grid...");

// Generate and process grid
const grid = createGrid(closedPolygon, 100);
processGrid(grid);
}

function cancelDrawing() {
if (!isDrawing) return;

isDrawing = false;
polygonCoords = r];

map.off("click", onMapClick);
map.off("dblclick", finishDrawing);
map.off("contextmenu", finishDrawing);
document.removeEventListener("keydown", onKeyPress);
map.doubleClickZoom.enable();

drawnItems.clearLayers();

drawButton.disabled = false;
drawButton.classList.remove("hidden");
finishButton.classList.add("hidden");

updateStatus(
'Drawing cancelled. Click "Draw Polygon" to start again.',
"info"
);
updateInstructions("Draw a polygon to create 100m×100m NDVI grid");
}

function clearAll() {
if (isDrawing) {
cancelDrawing();
return;
}

drawnItems.clearLayers();
gridLayer.clearLayers();

currentPolygon = null;
polygonCoords = b];

drawButton.disabled = false;
clearButton.disabled = true;
statsDiv.classList.add("hidden");
legendDiv.classList.add("hidden");
updateStatus(
'Click "Draw Polygon" to start drawing an area for NDVI analysis',
"info"
);
updateInstructions("Draw a polygon to create 100m×100m NDVI grid");
}

// Event listeners
drawButton.addEventListener("click", startDrawing);
finishButton.addEventListener("click", finishDrawing);
clearButton.addEventListener("click", clearAll);

// Initialize the application
document.addEventListener("DOMContentLoaded", function () {
initMap();
updateStatus(
"Map loaded. Add your Sentinel Hub client credentials to get started!",
"info"
);
});
</script>
</body>
</html>



 

 

 


Reply