Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Http;
trait LocationTrait
{
/**
* Get the profile's location and optionally generate a link to OpenStreetMap.
* Attention: do not extensively use the $lookUpOsmLocation as too many request will be rate-limited!
*
* @param bool $lookUpOsmLocation Whether to fetch OSM coordinates, URL and polygon data
* @return array
*/
public function getLocationFirst($lookUpOsmLocation = false)
{
// Initialize variables
$location = '';
$country = '';
$division = '';
$city = '';
$district = '';
$locationData = [];
if ($this->locations && $this->locations->isNotEmpty()) {
// Use the first location if available
$firstLocation = $this->locations->first();
} else {
// Fallback to the single location property
$firstLocation = $this->location;
}
if ($firstLocation) {
if (isset($firstLocation->city)) {
$cityTranslation = $firstLocation->city->translations->first();
$city = $cityTranslation ? $cityTranslation->name : '';
$location = $city;
}
if (isset($firstLocation->district)) {
$districtTranslation = $firstLocation->district->translations->first();
$district = $districtTranslation ? $districtTranslation->name : '';
$location = $city ? $city . ' ' . $district : $district;
}
if (isset($firstLocation->division)) {
$divisionTranslation = $firstLocation->division->translations->first();
$division = $divisionTranslation ? $divisionTranslation->name : '';
$location = $city || $district ? $location . ', ' . $division : $division;
}
if (isset($firstLocation->country)) {
if ($firstLocation->country->code === 'XX') {
$country = __('Location not specified');
$countryName = $country;
} else {
$country = $firstLocation->country->code;
$countryName = $firstLocation->country->translations->first()->name;
}
$location = $city || $district || $division ? $location . ', ' . $country : $country;
}
}
// Remove trailing comma and space
$locationName = rtrim($location, ', ');
// Remove any space before a comma
$locationName = preg_replace('/\s+,/', ',', $locationName);
$locationData['name'] = $locationName;
// Construct name_short based on available properties
$nameShortParts = [];
if ($city) {
$nameShortParts[] = $city;
}
if ($district && count($nameShortParts) < 2) {
$nameShortParts[] = $district;
}
if ($division && count($nameShortParts) < 2) {
$nameShortParts[] = $division;
}
if (!$city && $country && count($nameShortParts) < 2) {
$nameShortParts[] = $countryName;
}
// Join the parts with a space
$locationData['name_short'] = implode(' ', $nameShortParts);
// Remove any space before a comma
$locationData['name_short'] = preg_replace('/\s+,/', ',', $locationData['name_short']);
if ($lookUpOsmLocation == true) {
// Construct the URL for Nominatim search with polygon data
$searchUrl = 'https://nominatim.openstreetmap.org/search?format=json&polygon_geojson=1&q=' . urlencode($locationName);
// Define your User-Agent
$userAgent = config('app.name') . ' (' . timebank_config('mail.system_admin.email') . ')';
// Send the HTTP request to the Nominatim API
$response = Http::withHeaders([
'User-Agent' => $userAgent,
])->get($searchUrl);
// Parse the JSON response
$data = $response->json();
// Extract the first result's coordinates (if available)
if (!empty($data[0])) {
$latitude = $data[0]['lat'];
$longitude = $data[0]['lon'];
// Add coordinate data
$locationData['latitude'] = $latitude;
$locationData['longitude'] = $longitude;
// Add polygon data if available
if (isset($data[0]['geojson'])) {
$locationData['polygon'] = $data[0]['geojson'];
$locationData['polygon_type'] = $data[0]['geojson']['type'] ?? null;
// Create OpenStreetMap URL using polygon data (relation/way)
if (isset($data[0]['osm_type']) && isset($data[0]['osm_id'])) {
$osmType = $data[0]['osm_type'];
$osmId = $data[0]['osm_id'];
// Use relation or way URL for default polygon display with sidebar
if ($osmType === 'relation') {
$locationData['url'] = "https://www.openstreetmap.org/relation/{$osmId}";
} elseif ($osmType === 'way') {
$locationData['url'] = "https://www.openstreetmap.org/way/{$osmId}";
} else {
// Fallback to coordinate-based URL with center marker
$locationData['url'] = "https://www.openstreetmap.org/?mlat={$latitude}&mlon={$longitude}#map=12/{$latitude}/{$longitude}&layers=V";
}
} else {
// Fallback to coordinate-based URL with center marker
$locationData['url'] = "https://www.openstreetmap.org/?mlat={$latitude}&mlon={$longitude}#map=12/{$latitude}/{$longitude}&layers=V";
}
} else {
// No polygon data, use coordinate-based URL with center marker
$locationData['url'] = "https://www.openstreetmap.org/?mlat={$latitude}&mlon={$longitude}#map=12/{$latitude}/{$longitude}&layers=V";
}
// Add place information
$locationData['place_id'] = $data[0]['place_id'] ?? null;
$locationData['osm_type'] = $data[0]['osm_type'] ?? null;
$locationData['osm_id'] = $data[0]['osm_id'] ?? null;
$locationData['display_name'] = $data[0]['display_name'] ?? null;
} else {
$locationData['url'] = null; // No location found
}
}
return $locationData;
}
private function getOsmUrl($locationName)
{
$searchUrl = 'https://nominatim.openstreetmap.org/search?format=json&q=' . urlencode($locationName);
// Define your User-Agent
$userAgent = config('app.name') . ' (' . timebank_config('mail.system_admin.email') . ')';
// Send the HTTP request to the Nominatim API
$response = Http::withHeaders([
'User-Agent' => $userAgent,
])->get($searchUrl);
// Parse the JSON response
$data = $response->json();
// Extract the first result's coordinates (if available)
if (!empty($data[0])) {
$latitude = $data[0]['lat'];
$longitude = $data[0]['lon'];
// Create the OpenStreetMap URL with the coordinates
$locationData['url'] = "https://www.openstreetmap.org/?mlat={$latitude}&mlon={$longitude}#map=12/{$latitude}/{$longitude}";
} else {
$locationData['url'] = null; // No location found
}
return $locationData;
}
/**
* Get polygon boundary for a specific location
*
* @param string $locationName The location to search for
* @param array $options Additional search options (type, limit, etc.)
* @return array|null Returns polygon data or null if not found
*/
public function getLocationPolygon($locationName, $options = [])
{
// Default options
$defaultOptions = [
'type' => null, // e.g., 'city', 'administrative', 'boundary'
'limit' => 1,
'admin_level' => null, // e.g., 8 for cities, 4 for states
];
$options = array_merge($defaultOptions, $options);
// Construct the URL for Nominatim search
$searchUrl = 'https://nominatim.openstreetmap.org/search?format=json&polygon_geojson=1&q=' . urlencode($locationName);
// Add optional filters
if ($options['limit']) {
$searchUrl .= '&limit=' . $options['limit'];
}
// Define your User-Agent
$userAgent = config('app.name') . ' (' . timebank_config('mail.system_admin.email') . ')';
try {
// Send the HTTP request to the Nominatim API
$response = Http::timeout(10)->withHeaders([
'User-Agent' => $userAgent,
])->get($searchUrl);
// Parse the JSON response
$data = $response->json();
if (empty($data)) {
return null;
}
// Filter results by type if specified
if ($options['type']) {
$data = array_filter($data, function($item) use ($options) {
return isset($item['type']) && $item['type'] === $options['type'];
});
}
// Filter by admin level if specified
if ($options['admin_level']) {
$data = array_filter($data, function($item) use ($options) {
return isset($item['admin_level']) && $item['admin_level'] == $options['admin_level'];
});
}
// Get the first (best) result
$result = reset($data);
if (!$result || !isset($result['geojson'])) {
return null;
}
return [
'polygon' => $result['geojson'],
'polygon_type' => $result['geojson']['type'] ?? null,
'coordinates_count' => $this->countPolygonCoordinates($result['geojson']),
'latitude' => $result['lat'] ?? null,
'longitude' => $result['lon'] ?? null,
'display_name' => $result['display_name'] ?? null,
'place_id' => $result['place_id'] ?? null,
'osm_type' => $result['osm_type'] ?? null,
'osm_id' => $result['osm_id'] ?? null,
'boundingbox' => $result['boundingbox'] ?? null,
];
} catch (\Exception $e) {
// Log error if needed
\Log::warning('Nominatim polygon request failed: ' . $e->getMessage());
return null;
}
}
/**
* Count the number of coordinates in a GeoJSON polygon
* Useful for determining polygon complexity
*
* @param array $geojson
* @return int
*/
private function countPolygonCoordinates($geojson)
{
if (!isset($geojson['coordinates'])) {
return 0;
}
$count = 0;
if ($geojson['type'] === 'Polygon') {
foreach ($geojson['coordinates'] as $ring) {
$count += count($ring);
}
} elseif ($geojson['type'] === 'MultiPolygon') {
foreach ($geojson['coordinates'] as $polygon) {
foreach ($polygon as $ring) {
$count += count($ring);
}
}
}
return $count;
}
/**
* Get simplified polygon for display purposes
* Nominatim returns detailed polygons that might be too complex for some uses
*
* @param string $locationName
* @return array|null
*/
public function getSimplifiedLocationPolygon($locationName)
{
$polygon = $this->getLocationPolygon($locationName);
if (!$polygon || !$polygon['polygon']) {
return null;
}
// If polygon has too many coordinates, you might want to simplify it
// This is a basic example - you might want to use a proper simplification algorithm
if ($polygon['coordinates_count'] > 1000) {
// For very complex polygons, you might want to:
// 1. Use a different endpoint
// 2. Implement coordinate simplification
// 3. Use bounding box instead
$polygon['simplified'] = true;
$polygon['original_coordinates_count'] = $polygon['coordinates_count'];
}
return $polygon;
}
}