Hello,
I want a PHP function which has three parameters : $taxonKey, $latitude and $longitude and which returns the nearest occurence to my point in parameter and the distance in kilometers between the point in parameters and the occurence. I want a smart and quick function so which use predicates to avoid to browse all the occurences of the taxon. I want to use file_get_contents to access the API.
Thank you
PS I asked to chatGPT before but it answered anything, yes we haven’t been replaced !
I succeeded with this code :
function getNearestOccurrence($taxonKey, $latitude, $longitude, $buffer = 1) {
// URL de l’API GBIF pour les recherches avancées avec prédicats
$url = “https://api.gbif.org/v1/occurrence/search/predicate”;
// Définition des prédicats pour filtrer UNIQUEMENT les occurrences avec le bon TAXON_KEY
$predicate = [
"type" => "and",
"predicates" => [
["type" => "equals", "key" => "TAXON_KEY", "value" => (int) $taxonKey], // Correction du nom
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => (float) ($latitude - $buffer)],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => (float) ($latitude + $buffer)],
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => (float) ($longitude - $buffer)],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => (float) ($longitude + $buffer)]
]
];
// Encodage de la requête en JSON
$postData = json_encode([
"predicate" => $predicate,
"limit" => 100, // Limite de résultats
"sortBy" => "distance" // Trier par distance
]);
// Debugging : Afficher la requête envoyée pour vérification
echo "URL utilisée : " . $url . "\n";
echo "Requête envoyée : " . json_encode($postData, JSON_PRETTY_PRINT) . "\n";
// Options pour file_get_contents avec méthode POST
$options = [
"http" => [
"header" => "Content-Type: application/json\r\n",
"method" => "POST",
"content" => $postData
]
];
$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
// Vérification si la requête a réussi
if ($response === FALSE) {
return ["error" => "Impossible de récupérer les données de l'API GBIF."];
}
// Debugging : Afficher la réponse brute de l'API
// echo "Réponse API brute : " . $response . “\n”;
// Décodage de la réponse JSON
$data = json_decode($response, true);
// Vérification de la validité des résultats
if (!isset($data["results"]) || count($data["results"]) === 0) {
return ["error" => "Aucune occurrence trouvée pour ce taxon dans cette zone."];
}
// On retourne directement la première occurrence puisque l'API a déjà filtré
return [
"occurrence" => $data["results"][0],
"distance_km" => haversineDistance(
$latitude,
$longitude,
$data["results"][0]["decimalLatitude"],
$data["results"][0]["decimalLongitude"]
)
];
}
function haversineDistance($lat1, $lon1, $lat2, $lon2) {
$earthRadius = 6371; // Rayon moyen de la Terre en km
// Conversion des degrés en radians
$lat1 = deg2rad($lat1);
$lon1 = deg2rad($lon1);
$lat2 = deg2rad($lat2);
$lon2 = deg2rad($lon2);
// Différence de latitude et de longitude
$dlat = $lat2 - $lat1;
$dlon = $lon2 - $lon1;
// Formule de Haversine pour calculer la distance
$a = sin($dlat / 2) * sin($dlat / 2) + cos($lat1) * cos($lat2) * sin($dlon / 2) * sin($dlon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
$distance = $earthRadius * $c;
return $distance;
}
@Sylvain_Ard, it’s worthwhile to filter results first to remove or correct defective or missing coordinates, or those with coordinateUncertaintyInMeters larger than the distance of interest.
For small distances you can use Euclidean rather than Haversine distance, for example
[formula removed because forum software doesn’t display it properly!]
where “ctr” is the central point of interest and “rec” is the point from the record. I use a formula like this to find millipede records, as shown in the screenshot below, and note that my distance field also has direction.
this is my actual code :
function getNearestOccurrenceGBIF($taxonKey, $latitude, $longitude, $buffer_kms=300) {
$url = “https://api.gbif.org/v1/occurrence/search/predicate”;
// Calcul des buffers dynamiques pour couvrir environ $buffer_kms km
$buffer_lat = min($buffer_kms / 111,90); // 1° latitude ≈ 111 km
$cos_lat = max(cos(deg2rad($latitude)), 0.0001);
$buffer_lon = min($buffer_kms / (111 * $cos_lat),180);
// Calcul des bornes avant correction
$lat_min = $latitude - $buffer_lat;
$lat_max = $latitude + $buffer_lat;
$lon_min = $longitude - $buffer_lon;
$lon_max = $longitude + $buffer_lon;
// Gestion des intervalles de latitude avec OR si dépassement
if ($lat_min < -90 || $lat_max > 90) {
$lat_predicate = [
"type" => "or",
"predicates" => [
[
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => max($lat_min, -90)],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => 90]
]
],
[
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => -90],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => min($lat_max, 90)]
]
]
]
];
} else {
$lat_predicate = [
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => $lat_min],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LATITUDE", "value" => $lat_max]
]
];
}
// Gestion des intervalles de longitude avec OR si dépassement
if ($lon_min < -180 || $lon_max > 180) {
$lon_predicate = [
"type" => "or",
"predicates" => []
];
if ($lon_min < -180) {
$lon_predicate["predicates"][] = [
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => max($lon_min + 360, -180)],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => 180]
]
];
} else {
$lon_predicate["predicates"][] = [
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => $lon_min],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => 180]
]
];
}
if ($lon_max > 180) {
$lon_predicate["predicates"][] = [
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => -180],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => min($lon_max - 360, 180)]
]
];
} else {
$lon_predicate["predicates"][] = [
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => -180],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => $lon_max]
]
];
}
} else {
$lon_predicate = [
"type" => "and",
"predicates" => [
["type" => "greaterThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => $lon_min],
["type" => "lessThanOrEquals", "key" => "DECIMAL_LONGITUDE", "value" => $lon_max]
]
];
}
// Fusion des conditions pour l'API
$predicate = [
"type" => "and",
"predicates" => [
["type" => "equals", "key" => "TAXON_KEY", "value" => (int) $taxonKey],
$lat_predicate,
$lon_predicate
]
];
$postData = json_encode(["predicate" => $predicate, "limit" => 100, "sortBy" => "distance"]);
// Envoi de la requête
$options = [
"http" => [
"header" => "Content-Type: application/json\r\n",
"method" => "POST",
"content" => $postData
]
];
$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
if ($response === FALSE) {
return ["error" => "Impossible de récupérer les données de l'API GBIF."];
}
$data = json_decode($response, true);
if (!isset($data["results"]) || count($data["results"]) === 0) {
return ["error" => "Aucune occurrence trouvée pour ce taxon dans cette zone."];
}
// Sélection de l'occurrence la plus proche
$filteredOccurrences = [];
foreach ($data["results"] as $occurrence) {
if (!isset($occurrence["decimalLatitude"]) || !isset($occurrence["decimalLongitude"])) {
continue;
}
if (isset($occurrence["coordinateUncertaintyInMeters"]) && $occurrence["coordinateUncertaintyInMeters"] > 1000) {
continue;
}
if (isset($occurrence['issues']) && is_array($occurrence['issues']) && (in_array('COUNTRY_COORDINATE_MISMATCH',$occurrence['issues'])||in_array('ZERO_COORDINATE',$occurrence['issues'])))
continue;
$occ_lat = (float) $occurrence["decimalLatitude"];
$occ_lon = (float) $occurrence["decimalLongitude"];
// Calcul de la distance de Haversine
$haversine_distance = haversineDistance($latitude, $longitude, $occ_lat, $occ_lon);
// Si la distance est inférieure à 50 km, utiliser la distance euclidienne
if ($haversine_distance < 15) {
$distance = euclideanDistance($latitude, $longitude, $occ_lat, $occ_lon);
} else {
$distance = $haversine_distance;
}
$occurrence["distance_km"] = $distance;
$filteredOccurrences[] = $occurrence;
}
if (count($filteredOccurrences) === 0) {
return ["error" => "Aucune occurrence avec des coordonnées précises."];
}
// Trier par distance
usort($filteredOccurrences, function($a, $b) {
return $a["distance_km"] <=> $b["distance_km"];
});
return [
"occurrence" => $filteredOccurrences[0],
"distance_km" => $filteredOccurrences[0]["distance_km"]
];
}
function haversineDistance($lat1, $lon1, $lat2, $lon2) {
$earthRadius = 6371; // Rayon moyen de la Terre en km
// Conversion des degrés en radians
$lat1 = deg2rad($lat1);
$lon1 = deg2rad($lon1);
$lat2 = deg2rad($lat2);
$lon2 = deg2rad($lon2);
// Différence de latitude et de longitude
$dlat = $lat2 - $lat1;
$dlon = $lon2 - $lon1;
// Formule de Haversine pour calculer la distance
$a = sin($dlat / 2) * sin($dlat / 2) + cos($lat1) * cos($lat2) * sin($dlon / 2) * sin($dlon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
$distance = $earthRadius * $c;
return $distance;
}
function euclideanDistance($lat1, $lon1, $lat2, $lon2) {
$avg_lat = deg2rad(($lat1 + $lat2) / 2); // Latitude moyenne en radians
$dx = ($lat2 - $lat1) * 111;
$dy = ($lon2 - $lon1) * 111 * cos($avg_lat);
return sqrt($dx * $dx + $dy * $dy);
}