diff --git a/.env.example b/.env.example index 192b898f..c816726e 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,7 @@ SESSION_LIFETIME=120 MEMCACHED_HOST=127.0.0.1 -REDIS_HOST=127.0.0.1 +REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 @@ -55,3 +55,5 @@ VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +CIPHERSWEET_KEY=b130a919cba9b7745d4b0144533308f2ffa38476cf562175aab8ca9f0e9648f1 diff --git a/README.md b/README.md index 5c8b8e14..338f8723 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,21 @@ docker run --rm \ ```vendor/bin/sail up -d``` +### Migrate and seed the database + +```./vendor/bin/sail artisan migrate:fresh --seed``` + #### Install node dependencies ```vendor/bin/sail yarn install``` -#### Start compiling watcher +#### Start just in time compiler ```vendor/bin/sail yarn dev``` -#### Compile assets - -```vendor/bin/sail yarn build``` - #### Update dependencies -```vendor/bin/sail yarn install``` +```vendor/bin/sail yarn``` ## Contributing diff --git a/app/Http/Livewire/Meetup/PrepareForBtcMapItem.php b/app/Http/Livewire/Meetup/PrepareForBtcMapItem.php index deb25142..9b9e946b 100644 --- a/app/Http/Livewire/Meetup/PrepareForBtcMapItem.php +++ b/app/Http/Livewire/Meetup/PrepareForBtcMapItem.php @@ -3,6 +3,7 @@ namespace App\Http\Livewire\Meetup; use App\Models\Meetup; +use Illuminate\Http\Client\Pool; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; use Livewire\Component; @@ -14,138 +15,322 @@ class PrepareForBtcMapItem extends Component public Meetup $meetup; + public $model; + + public string $search = ''; + public $population; - public $population_date; - public $osmSearchResults; - public $osmSearchResultsState; - public $osmSearchResultsCountry; + public $population_date = ''; - public $selectedItem; + public ?int $osm_id = null; - public function rules() + public array $osmSearchResults = []; + + public $selectedItemOSMPolygons; + + public $selectedItemOSMBoundaries; + + public $selectedItemPolygonsOSMfr; + + public $polygonsOSMfrX = 0.020000; + + public $polygonsOSMfrY = 0.005000; + + public $polygonsOSMfrZ = 0.005000; + + public $currentPercentage = 100; + + public bool $OSMBoundaries = false; + + public bool $polygonsOSMfr = false; + + protected $queryString = [ + 'search' => ['except' => ''], + 'osm_id' => ['except' => null], + ]; + + public function rules(): array { return [ - 'population' => 'required', + 'search' => 'required|string', + 'currentPercentage' => 'required|numeric', + + 'model.simplified_geojson' => 'nullable', + + 'OSMBoundaries' => 'bool', + 'polygonsOSMfr' => 'bool', + + 'population' => 'required|numeric', 'population_date' => 'required|string', + + 'polygonsOSMfrX' => 'numeric|max:1', + 'polygonsOSMfrY' => 'numeric|max:1', + 'polygonsOSMfrZ' => 'numeric|max:1', ]; } - public function mount() + public function mount(): void { - $response = Http::acceptJson() - ->get( - 'https://nominatim.openstreetmap.org/search?city='.$this->meetup->city->name.'&format=json&polygon_geojson=1' - ); - $this->osmSearchResults = $response->json(); - - $response = Http::acceptJson() - ->get( - 'https://nominatim.openstreetmap.org/search?state='.$this->meetup->city->name.'&format=json&polygon_geojson=1' - ); - $this->osmSearchResultsState = $response->json(); - - $response = Http::acceptJson() - ->get( - 'https://nominatim.openstreetmap.org/search?country='.$this->meetup->city->name.'&format=json&polygon_geojson=1' - ); - $this->osmSearchResultsCountry = $response->json(); - - if ($this->meetup->city->osm_relation) { - $this->selectedItem = $this->meetup->city->osm_relation; + $this->model = $this->meetup->city; + $this->population = $this->model->population; + $this->population_date = $this->model->population_date; + $this->getSearchResults(); + if ($this->osm_id) { + $this->selectedItemOSMPolygons = collect($this->osmSearchResults) + ->firstWhere('osm_id', $this->osm_id); + $this->executeMapshaper($this->currentPercentage); } - $this->population = $this->meetup->city->population; - $this->population_date = $this->meetup->city->population_date; + } + + private function getSearchResults(): void + { + $responses = Http::pool(fn(Pool $pool) => [ + $pool->acceptJson() + ->get( + sprintf('https://nominatim.openstreetmap.org/search?q=%s&format=json&polygon_geojson=1&polygon_threshold=0.0003&email='.config('services.nominatim.email'), + $this->search) + ), + ]); + + $this->osmSearchResults = collect($responses[0]->json()) + ->filter(fn($item + ) => ( + $item['geojson']['type'] === 'Polygon' + || $item['geojson']['type'] === 'MultiPolygon' + ) + && $item['osm_id'] + && count($item['geojson']['coordinates'], COUNT_RECURSIVE) < 100000 + ) + ->values() + ->toArray(); + } + + private function executeMapshaper($percentage = 100): void + { + try { + // put OSM geojson to storage + Storage::disk('geo') + ->put('geojson_'.$this->selectedItemOSMPolygons['osm_id'].'.json', + json_encode($this->selectedItemOSMPolygons['geojson'], JSON_THROW_ON_ERROR) + ); + + // execute mapshaper + $input = storage_path('app/geo/geojson_'.$this->selectedItemOSMPolygons['osm_id'].'.json'); + $output = storage_path('app/geo/output_'.$this->selectedItemOSMPolygons['osm_id'].'.json'); + $mapShaperBinary = base_path('node_modules/mapshaper/bin/mapshaper'); + exec($mapShaperBinary.' '.$input.' -simplify dp '.$percentage.'% -o '.$output); + $this->currentPercentage = $percentage; + + $mapShaperOutput = str( + Storage::disk('geo') + ->get('output_'.$this->selectedItemOSMPolygons['osm_id'].'.json') + ); + if ($mapShaperOutput->contains(['Polygon', 'MultiPolygon'])) { + // trim geojson + Storage::disk('geo') + ->put( + 'trimmed_'.$this->selectedItemOSMPolygons['osm_id'].'.json', + $mapShaperOutput + ->after('{"type":"GeometryCollection", "geometries": [') + ->beforeLast(']}') + ->toString() + ); + } else { + $this->notification() + ->warning('Warning', + sprintf('Geojson is not valid. After simplification, it contains no polygons. Instead it contains: %s', + $mapShaperOutput->after('{"type":') + ->before(','))); + + return; + } + + // put trimmed geojson to model + $this->model->simplified_geojson = json_decode( + trim( + Storage::disk('geo') + ->get('trimmed_'.$this->selectedItemOSMPolygons['osm_id'].'.json') + ), + false, 512, JSON_THROW_ON_ERROR + ); + + // emit event for AlpineJS + $this->emit('geoJsonUpdated'); + + } catch (\Exception $e) { + $this->notification() + ->error('Error', $e->getMessage()); + } + } + + public function saveSimplifiedGeoJson() + { + $this->model->osm_relation = $this->osm_id; + $this->model->save(); + + $this->notification() + ->success('Success', 'Simplified GeoJSON saved.'); + } + + public function submitPolygonsOSM() + { + $this->validate(); + $postGenerate = Http::acceptJson() + ->asForm() + ->post( + 'https://polygons.openstreetmap.fr/?id='.$this->selectedItemOSMPolygons['osm_id'], + [ + 'x' => $this->polygonsOSMfrX, + 'y' => $this->polygonsOSMfrY, + 'z' => $this->polygonsOSMfrZ, + 'generate' => 'Submit+Query', + ] + ); + if ($postGenerate->ok()) { + $getUrl = sprintf( + 'https://polygons.openstreetmap.fr/get_geojson.py?id=%s¶ms=%s-%s-%s', + $this->selectedItemOSMPolygons['osm_id'], + (float) str($this->polygonsOSMfrX) + ->before('.') + ->toString().'.'.str($this->polygonsOSMfrX) + ->after('.') + ->padRight(6, '0') + ->toString(), + (float) str($this->polygonsOSMfrY) + ->before('.') + ->toString().'.'.str($this->polygonsOSMfrY) + ->after('.') + ->padRight(6, '0') + ->toString(), + (float) str($this->polygonsOSMfrZ) + ->before('.') + ->toString().'.'.str($this->polygonsOSMfrZ) + ->after('.') + ->padRight(6, '0') + ->toString(), + ); + $response = Http::acceptJson() + ->get($getUrl); + if ($response->json()) { + $this->selectedItemPolygonsOSMfr = $response->json(); + $this->emit('geoJsonUpdated'); + } else { + $this->notification() + ->warning('No data', 'No data found for this area.'); + } + } else { + $this->notification() + ->error('Error', 'Something went wrong: '.$postGenerate->status()); + } + } + + public function submit(): void + { + $this->validate(); + $this->getSearchResults(); + } + + public function selectItem($index): void + { + $this->OSMBoundaries = false; + $this->selectedItemOSMBoundaries = null; + $this->selectedItemOSMPolygons = $this->osmSearchResults[$index]; + $this->osm_id = $this->selectedItemOSMPolygons['osm_id']; + $this->model->osm_relation = $this->selectedItemOSMPolygons; + + $this->executeMapshaper(100); + } + + public function updatedOSMBoundaries($value) + { + if ($value) { + $response = Http::acceptJson() + ->asForm() + ->post('https://osm-boundaries.com/Ajax/GetBoundary', [ + 'db' => 'osm20221205', + 'waterOrLand' => 'water', + 'osmId' => '-'.$this->selectedItemOSMPolygons['osm_id'], + ]); + if ($response->json()) { + if (count($response->json()['coordinates'], COUNT_RECURSIVE) > 100000) { + $this->notification() + ->warning('Warning', 'Water boundaries are too big'); + + return; + } + + $this->selectedItemOSMBoundaries = $response->json(); + $this->emit('geoJsonUpdated'); + } else { + $this->notification() + ->warning('Warning', 'No water boundaries found'); + } + } else { + $this->selectedItemOSMBoundaries = null; + $this->emit('geoJsonUpdated'); + } + } + + public function updatedCurrentPercentage($value) + { + $this->executeMapshaper((float) $value); + } + + public function setPercentage($percent): void + { + $this->executeMapshaper($percent); } public function updatedPopulation($value) { - $value = str($value) - ->replace('.', '') - ->replace(',', '.') - ->toInteger(); - $this->meetup->city->population = $value; - $this->population = $value; - $this->meetup->city->save(); + $this->model->population = (int) str($value) + ->replace(['.', ','], '') + ->toString(); + $this->model->save(); + $this->notification() - ->success('Population updated', 'Success'); + ->success('Success', 'Population saved.'); } public function updatedPopulationDate($value) { - $this->meetup->city->population_date = $value; - $this->meetup->city->save(); + $this->model->population_date = $value; + $this->model->save(); + $this->notification() - ->success('Population date updated', 'Success'); - } - - public function selectItem($index, bool $isState = false, $isCountry = false) - { - if ($isState) { - $this->selectedItem = $this->osmSearchResultsState[$index]; - } elseif ($isCountry) { - $this->selectedItem = $this->osmSearchResultsCountry[$index]; - } else { - $this->selectedItem = $this->osmSearchResults[$index]; - } - Storage::disk('geo') - ->put('geojson_'.$this->selectedItem['osm_id'].'.json', - json_encode($this->selectedItem['geojson'], JSON_THROW_ON_ERROR)); - $input = storage_path('app/geo/geojson_'.$this->selectedItem['osm_id'].'.json'); - $output = storage_path('app/geo/output_'.$this->selectedItem['osm_id'].'.json'); - exec('mapshaper '.$input.' -simplify dp 4% -o '.$output); - Storage::disk('geo') - ->put( - 'trimmed_'.$this->selectedItem['osm_id'].'.json', - str(Storage::disk('geo') - ->get('output_'.$this->selectedItem['osm_id'].'.json')) - ->after('{"type":"GeometryCollection", "geometries": [') - ->beforeLast(']}') - ->toString() - ); - $this->meetup->city->osm_relation = $this->selectedItem; - $this->meetup->city->simplified_geojson = json_decode(trim(Storage::disk('geo') - ->get('trimmed_'.$this->selectedItem['osm_id'].'.json')), - false, 512, JSON_THROW_ON_ERROR); - $this->meetup->city->population = 0; - $this->meetup->city->population_date = '2021-12-31'; - $this->meetup->city->save(); - - return to_route('osm.meetups.item', ['meetup' => $this->meetup]); - } - - public function setPercent($percent) - { - $input = storage_path('app/geo/geojson_'.$this->selectedItem['osm_id'].'.json'); - $output = storage_path('app/geo/output_'.$this->selectedItem['osm_id'].'.json'); - exec('mapshaper '.$input.' -simplify dp '.$percent.'% -o '.$output); - Storage::disk('geo') - ->put( - 'trimmed_'.$this->selectedItem['osm_id'].'.json', - str(Storage::disk('geo') - ->get('output_'.$this->selectedItem['osm_id'].'.json')) - ->after('{"type":"GeometryCollection", "geometries": [') - ->beforeLast(']}') - ->toString() - ); - $this->meetup->city->simplified_geojson = json_decode(trim(Storage::disk('geo') - ->get('trimmed_'.$this->selectedItem['osm_id'].'.json')), - false, 512, JSON_THROW_ON_ERROR); - $this->meetup->city->save(); - - return to_route('osm.meetups.item', ['meetup' => $this->meetup]); - } - - public function takePop($value) - { - $this->meetup->city->population = $value; - $this->meetup->city->population_date = '2021-12-31'; - $this->meetup->city->save(); - - return to_route('osm.meetups.item', ['meetup' => $this->meetup]); + ->success('Success', 'Population saved.'); } public function render() { - return view('livewire.meetup.prepare-for-btc-map-item'); + return view('livewire.meetup.prepare-for-btc-map-item', [ + 'percentages' => collect([ + 0.5, + 0.75, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 15, + 20, + 25, + 30, + 40, + 50, + 60, + 75, + 80, + 100, + ]) + ->reverse() + ->values() + ->toArray(), + ]); } } diff --git a/app/Http/Livewire/Tables/MeetupForBtcMapTable.php b/app/Http/Livewire/Tables/MeetupForBtcMapTable.php index 3e24ae2c..2ad6c36a 100644 --- a/app/Http/Livewire/Tables/MeetupForBtcMapTable.php +++ b/app/Http/Livewire/Tables/MeetupForBtcMapTable.php @@ -18,6 +18,7 @@ class MeetupForBtcMapTable extends DataTableComponent 'simplified_geojson', 'population', 'population_date', + 'city_id', ]) ->setPerPageAccepted([ 100000, diff --git a/app/Models/Meetup.php b/app/Models/Meetup.php index 5f5dd6a4..3a3ce481 100644 --- a/app/Models/Meetup.php +++ b/app/Models/Meetup.php @@ -30,9 +30,10 @@ class Meetup extends Model implements HasMedia * @var array */ protected $casts = [ - 'id' => 'integer', - 'city_id' => 'integer', - 'github_data' => 'json', + 'id' => 'integer', + 'city_id' => 'integer', + 'github_data' => 'json', + 'simplified_geojson' => 'array', ]; protected static function booted() diff --git a/resources/views/columns/meetups/osm-actions.blade.php b/resources/views/columns/meetups/osm-actions.blade.php index 7ba6561a..32b92c5b 100644 --- a/resources/views/columns/meetups/osm-actions.blade.php +++ b/resources/views/columns/meetups/osm-actions.blade.php @@ -16,7 +16,7 @@ Open OSM Item diff --git a/resources/views/livewire/meetup/prepare-for-btc-map-item.blade.php b/resources/views/livewire/meetup/prepare-for-btc-map-item.blade.php index 8e0627d6..4d0581e8 100644 --- a/resources/views/livewire/meetup/prepare-for-btc-map-item.blade.php +++ b/resources/views/livewire/meetup/prepare-for-btc-map-item.blade.php @@ -1,118 +1,444 @@ -
-
- Zurück -
-
-

Search city: {{ $meetup->city->name }}

-

OSM API Response

-
- @foreach($osmSearchResults as $item) - -
- {{ $item['display_name'] }} -
-
- @endforeach -
-
-
-

Search state: {{ $meetup->city->name }}

-

OSM API Response

-
- @foreach($osmSearchResultsState as $item) - -
- {{ $item['display_name'] }} -
-
- @endforeach -
-
-
-

Search country: {{ $meetup->city->name }}

-

OSM API Response

-
- @foreach($osmSearchResultsCountry as $item) - -
- {{ $item['display_name'] }} -
-
- @endforeach -
-
-
-
- @if($selectedItem) - geojson created - @endif -
-

Current data [points: {{ count($meetup->city->simplified_geojson['coordinates'][0] ?? []) }}]

-
-
7%
-
6%
-
5%
-
4%
-
3%
-
2%
-
1%
-
0.75%
-
0.5%
-
-
- @if($meetup->city->simplified_geojson) -

Simplified geojson

-
{{ json_encode($meetup->city->simplified_geojson, JSON_THROW_ON_ERROR) }}
-
+
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); + {{-- SEARCH PANEL --}} +
+
+
+

Search for an area +

+
- var geojsonFeature = { - 'type': 'Feature', - 'geometry': @js($meetup->city->simplified_geojson) - }; - console.log(geojsonFeature); - L.geoJSON(geojsonFeature).addTo(map); - let geoJSON = L.geoJson(geojsonFeature).addTo(map); - map.fitBounds(geoJSON.getBounds()); - } - }"> -
+
+ @if (!$model?->simplified_geojson || !$selectedItemOSMPolygons) +
+
+ +
+
+ Search +
+
+ @else +
+ + + Back + + +
+
+

+ {{ $selectedItemOSMPolygons['display_name'] }} +

+

+ +

+
+
+
+
+
+ {{ $selectedItemOSMPolygons['type'] }} +
+
+ OSM ID: {{ $selectedItemOSMPolygons['osm_id'] }} +
+
+
+
+
+
+
+
+ +
+
+ +
+ + + +
+
+

+ X, Y, Z are parameters for the following PostGIS equation. + The default values are chosen according to the size of the + original geometry to give a slighty bigger geometry, without + too many nodes. + +

+

Note that:

+

+ X > 0 will give a polygon bigger than the original geometry, + and guaranteed to contain it. +

+

+ X = 0 will give a polygon similar to the original geometry. +

+

+ X < 0 will give a polygon smaller than the original + geometry, and guaranteed to be smaller.

+
+
+ +
+
+
+
+
+ @endif +
+ @if (!$model?->simplified_geojson && $search) + + Now select the appropriate place so that a GeoJSON can be built. + + @endif +
+
+ +
+
+
+ @if ($search) +

+ Search: {{ $search }} +

+ @endif +
+ +
+
+
    + + @foreach ($osmSearchResults as $item) + @php + $currentClass = $item['osm_id'] === $osm_id ? 'bg-amber-400 dark:bg-amber-900' : ''; + @endphp + +
  • +
    +
    +

    + {{ $item['display_name'] }}

    +

    + + {{ count($item['geojson']['coordinates'], COUNT_RECURSIVE) }} + points + +

    +
    +
    + {{ $item['type'] }} +
    +
    +
  • + @endforeach +
+
+
+ +
+
+
+
+ + {{-- Wikipedia Links --}} +
+ @if ($search) + + @endif +
+ + +
+
+ + {{-- GeoJSON simplification --}} + @if ($selectedItemOSMPolygons) +
+
+
+

+ Mapshaper simplification of OSM GeoJSON + [{{ count($selectedItemOSMPolygons['geojson']['coordinates'], COUNT_RECURSIVE) }} + points] to + {{ count($model->simplified_geojson['coordinates'], COUNT_RECURSIVE) }} points +

+
+
+
+

+ (smaller percentage means fewer points - aim for no more than 150) +

+ +
+ +
+
+ +
+
@endif + + {{-- GeoJSON data --}} +
+ @if ($model?->simplified_geojson && $selectedItemOSMPolygons) +
+
+
+ @php + $jsonEncodedSelectedItem = json_encode($selectedItemOSMPolygons['geojson'], JSON_THROW_ON_ERROR); + @endphp +

+ OSM GeoJSON + [{{ count($selectedItemOSMPolygons['geojson']['coordinates'], COUNT_RECURSIVE) }} + points] +

+
+
+
{{ $jsonEncodedSelectedItem }}
+
+ + Copy to clipboard + +
+
+
+
+
+ @php + $jsonEncodedSimplifiedGeoJson = json_encode($model->simplified_geojson, JSON_THROW_ON_ERROR); + @endphp +

+ Simplified GeoJSON + [{{ count($model->simplified_geojson['coordinates'], COUNT_RECURSIVE) }} points] +

+
+
+
{{ $jsonEncodedSimplifiedGeoJson }}
+
+ + Save on model + +
+
+
+
+ @if ($selectedItemOSMBoundaries) +
+ @php + $jsonEncodedGeoJsonWater = json_encode($selectedItemOSMBoundaries, JSON_THROW_ON_ERROR); + @endphp +

+ https://osm-boundaries.com water GeoJSON + [{{ count($selectedItemOSMBoundaries['coordinates'], COUNT_RECURSIVE) }} + points] +

+
+
+
{{ $jsonEncodedGeoJsonWater }}
+
+ + Copy to clipboard + +
+
+
+
+ @endif + @if ($selectedItemPolygonsOSMfr) +
+ @php + $jsonEncodedGeoJsonOSMFr = json_encode($selectedItemPolygonsOSMfr, JSON_THROW_ON_ERROR); + @endphp +

+ https://polygons.openstreetmap.fr GeoJSON + + @if ($selectedItemPolygonsOSMfr['type'] !== 'GeometryCollection') + [{{ count($selectedItemPolygonsOSMfr['coordinates'] ?? [], COUNT_RECURSIVE) }} + points] + @endif + +

+
+
+
{{ $jsonEncodedGeoJsonOSMFr }}
+
+ + Copy to clipboard + +
+
+
+
+ @endif +
+ +
+
+
+

+ GeoJSON preview +

+
+
+
+
+
+
+
+ @endif +
+ +
+

+ GeoJSON helper is maintained by HolgerHatGarKeineNode [npub1pt0kw36ue3w2g4haxq3wgm6a2fhtptmzsjlc2j2vphtcgle72qesgpjyc6]. + This + software is open-sourced software + licensed under the MIT license. +

+
-
- @if($meetup->city->osm_relation) - - osm_id: {{ $meetup->city->osm_relation['osm_id'] }} - - - display_name: {{ $meetup->city->osm_relation['display_name'] }} - - @endif -
-

Wikipedia Search Results

- -

DB population

- - population: {{ $meetup->city->population }} - - - population date: {{ $meetup->city->population_date }} -
+ +