This commit is contained in:
Benjamin Takats
2023-01-28 21:48:20 +01:00
parent b9f3f0ba7b
commit 9f153e7630
23 changed files with 441 additions and 19 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands\Map;
use App\Models\Meetup;
use Illuminate\Console\Command;
class CreateGeoJsonPolygon extends Command
{
/**
* The name and signature of the console command.
* @var string
*/
protected $signature = 'map:polygon';
/**
* The console command description.
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
* @return int
*/
public function handle()
{
$meetups = Meetup::query()
->with([
'city',
])
->get();
foreach ($meetups as $meetup) {
dd($meetup->city->name);
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Http\Livewire\Meetup;
use App\Models\Meetup;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class PrepareForBtcMapItem extends Component
{
public Meetup $meetup;
public $wikipediaSearchResults;
public $osmSearchResults;
public $osmSearchResultsState;
public $selectedItem;
public function mount()
{
$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();
if ($this->meetup->city->osm_relation) {
$this->selectedItem = $this->meetup->city->osm_relation;
$wikipediaUrl = 'https://query.wikidata.org/sparql?query=SELECT%20%3Fpopulation%20WHERE%20%7B%0A%20%20SERVICE%20wikibase%3Amwapi%20%7B%0A%20%20%20%20%20%20bd%3AserviceParam%20mwapi%3Asearch%20%22'.urlencode($this->meetup->city->name).'%22%20.%20%20%20%20%0A%20%20%20%20%20%20bd%3AserviceParam%20mwapi%3Alanguage%20%22en%22%20.%20%20%20%20%0A%20%20%20%20%20%20bd%3AserviceParam%20wikibase%3Aapi%20%22EntitySearch%22%20.%0A%20%20%20%20%20%20bd%3AserviceParam%20wikibase%3Aendpoint%20%22www.wikidata.org%22%20.%0A%20%20%20%20%20%20bd%3AserviceParam%20wikibase%3Alimit%201%20.%0A%20%20%20%20%20%20%3Fitem%20wikibase%3AapiOutputItem%20mwapi%3Aitem%20.%0A%20%20%7D%0A%20%20%3Fitem%20wdt%3AP1082%20%3Fpopulation%0A%7D';
$response = Http::acceptJson()
->get(
$wikipediaUrl
);
$this->wikipediaSearchResults = $response->json();
}
}
public function selectItem($index, bool $isState = false)
{
if ($isState) {
$this->selectedItem = $this->osmSearchResultsState[$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 = date('Y');
$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 = date('Y');
$this->meetup->city->save();
return to_route('osm.meetups.item', ['meetup' => $this->meetup]);
}
public function render()
{
return view('livewire.meetup.prepare-for-btc-map-item');
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Livewire\Meetup;
use Livewire\Component;
class PrepareForBtcMapTable extends Component
{
public function render()
{
return view('livewire.meetup.prepare-for-btc-map-table');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Livewire\Tables;
use App\Models\Meetup;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
class MeetupForBtcMapTable extends DataTableComponent
{
public function configure(): void
{
$this
->setPrimaryKey('id')
->setAdditionalSelects([
'osm_relation',
'simplified_geojson',
'population',
'population_date',
])
->setPerPageAccepted([
100000,
])
->setPerPage(100000);
}
public function columns(): array
{
return [
Column::make("Id", "id")
->sortable(),
Column::make("Name", "name")
->sortable(),
Column::make("City", "city.name")
->sortable(),
Column::make("Country", "city.country.name")
->sortable(),
Column::make("Actions")
->label(fn($row, Column $column) => view('columns.meetups.osm-actions', ['row' => $row])),
];
}
public function builder(): Builder
{
return Meetup::query()
->with([
'city.country',
]);
}
}

View File

@@ -28,8 +28,10 @@ class City extends Model
* @var array
*/
protected $casts = [
'id' => 'integer',
'country_id' => 'integer',
'id' => 'integer',
'country_id' => 'integer',
'osm_relation' => 'json',
'simplified_geojson' => 'json',
];
protected static function booted()

View File

@@ -36,6 +36,12 @@ return [
'throw' => false,
],
'geo' => [
'driver' => 'local',
'root' => storage_path('app/geo'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
* @return void
*/
public function up()
{
Schema::table('cities', function (Blueprint $table) {
$table->json('osm_relation')
->nullable();
$table->json('simplified_geojson')
->nullable();
$table->unsignedBigInteger('population')
->nullable();
$table->string('population_date')
->nullable();
});
}
/**
* Reverse the migrations.
* @return void
*/
public function down()
{
Schema::table('cities', function (Blueprint $table) {
//
});
}
};

View File

@@ -125,6 +125,13 @@ class DatabaseSeeder extends Seeder
'longitude' => 8.65639,
'created_by' => 1,
]);
City::create([
'country_id' => 1,
'name' => 'Hessen',
'latitude' => 50.526501,
'longitude' => 9.004440,
'created_by' => 1,
]);
Venue::create([
'city_id' => 1,
'name' => 'The Blue Studio Coworking (Füssen)',
@@ -376,9 +383,9 @@ Deshalb werden Sie von mir in diesem Kurs leicht verständlich an das Thema hera
Artisan::call(ReadAndSyncPodcastFeeds::class);
Artisan::call(SyncOpenBooks::class);
Meetup::create([
'city_id' => 1,
'name' => 'Einundzwanzig ' . str()->random(5),
'telegram_link' => 'https://t.me/EinundzwanzigKempten',
'city_id' => 6,
'name' => 'Einundzwanzig Hessen',
'telegram_link' => 'https://t.me/EinundzwanzigHessen',
'created_by' => 1,
]);
MeetupEvent::create([

View File

@@ -697,5 +697,6 @@
"no location set": "kein Ort gesetzt",
"Open on Youtube": "Auf Youtube öffnen",
"You do not have permission to view the page.": "Du hast keine Berechtigung, die Seite anzuzeigen.",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "Bitte kontaktiere die Administratoren für neue Dateitypen, ansonsten packe die Dateien in ein ZIP! (Derzeit: PDF, ZIP)"
}
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "Bitte kontaktiere die Administratoren für neue Dateitypen, ansonsten packe die Dateien in ein ZIP! (Derzeit: PDF, ZIP)",
"load more...": ""
}

View File

@@ -691,5 +691,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
}
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -691,5 +691,6 @@
"no location set": "no hay ubicación establecida",
"Open on Youtube": "Abrir en Youtube",
"You do not have permission to view the page.": "No tienes permiso para ver la página.",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "Si deseas nuevos tipos de archivo, pónte en contacto con los administradores; de lo contrario, envíe los archivos en un ZIP. (Actualmente: PDF, ZIP)"
}
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "Si deseas nuevos tipos de archivo, pónte en contacto con los administradores; de lo contrario, envíe los archivos en un ZIP. (Actualmente: PDF, ZIP)",
"load more...": ""
}

View File

@@ -692,5 +692,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -692,5 +692,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -692,5 +692,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -692,5 +692,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -692,5 +692,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -692,5 +692,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -666,5 +666,6 @@
"no location set": "",
"Open on Youtube": "",
"You do not have permission to view the page.": "",
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": ""
"Please contact the admins for new file types, otherwise pack the files in a ZIP! (Currently: PDF, ZIP)": "",
"load more...": ""
}

View File

@@ -0,0 +1,24 @@
<div class="flex flex-col space-y-1">
<div>
@if($row->osm_relation)
has OSM relation
@endif
</div>
<div>
population {{ $row->population }}
</div>
<div>
@if($row->population_date)
population date {{ $row->population_date }}
@endif
</div>
<div>
<x-button
xs
amber
:href="route('osm.meetups.item', ['meetup' => $row])"
>
Open OSM Item
</x-button>
</div>
</div>

View File

@@ -29,6 +29,7 @@
<script src="{{ asset('vendor/jvector/maps/pl.js') }}"></script>
<script src="https://kit.fontawesome.com/03bc14bd1e.js" crossorigin="anonymous"></script>
<script src="{{ asset('dist/smoothscroll.js') }}"></script>
@mapstyles
@mapscripts
<wireui:scripts/>
<x-comments::scripts/>
@@ -37,7 +38,6 @@
<x-comments::styles/>
<x-embed-styles />
@livewireStyles
@mapstyles
<style>
.comments {
--comments-color-background: rgb(34, 34, 34);

View File

@@ -0,0 +1,104 @@
<div class="p-6 w-full" wire:loading.class="opacity-50">
<div class="max-w-none text-white flex flex-col space-y-4">
<a href="{{ route('osm.meetups') }}">Zurück</a>
<div class="grid grid-cols-2 gap-2">
<div>
<h1>Search city: {{ $meetup->city->name }}</h1>
<h1>OSM API Response</h1>
<div class="flex flex-col space-y-2">
@foreach($osmSearchResults as $item)
<code class="w-full">
<div wire:key="osmItemCity_{{ $loop->index }}" class="cursor-pointer underline" wire:click="selectItem({{ $loop->index }})">
{{ $item['display_name'] }}
</div>
</code>
@endforeach
</div>
</div>
<div>
<h1>Search state: {{ $meetup->city->name }}</h1>
<h1>OSM API Response</h1>
<div class="flex flex-col space-y-2">
@foreach($osmSearchResultsState as $item)
<code class="w-full">
<div wire:key="osmItemState_{{ $loop->index }}" class="cursor-pointer underline" wire:click="selectItem({{ $loop->index }}, true)">
{{ $item['display_name'] }}
</div>
</code>
@endforeach
</div>
</div>
</div>
<div>
@if($selectedItem)
geojson created
@endif
</div>
<h1>Current data [points: {{ count($meetup->city->simplified_geojson['coordinates'][0] ?? []) }}]</h1>
<div class="flex space-x-2">
<div class="cursor-pointer font-bold underline" wire:click="setPercent(7)">7%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(6)">6%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(5)">5%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(4)">4%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(3)">3%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(2)">2%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(1)">1%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(0.75)">0.75%</div>
<div class="cursor-pointer font-bold underline" wire:click="setPercent(0.5)">0.5%</div>
</div>
<div>
@if($meetup->city->simplified_geojson)
<h1>Simplified geojson</h1>
<pre
class="overflow-x-auto py-4">{{ json_encode($meetup->city->simplified_geojson, JSON_THROW_ON_ERROR) }}</pre>
<div
class="my-4"
x-data="{
init() {
var map = L.map($refs.map)
.setView([{{ $meetup->city->longitude }}, {{ $meetup->city->latitude }}], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
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());
}
}">
<div x-ref="map" style="width: 80vw; height: 30vh;"></div>
</div>
@endif
</div>
<div class="flex flex-col">
@if($meetup->city->osm_relation)
<code>
osm_id: {{ $meetup->city->osm_relation['osm_id'] }}
</code>
<code>
display_name: {{ $meetup->city->osm_relation['display_name'] }}
</code>
@endif
</div>
<h1>Wikipedia Search Results</h1>
<div class="flex space-x-2">
@foreach($wikipediaSearchResults['results']['bindings'] ?? [] as $pop)
<div wire:key="pop_{{ $loop->index }}" class="cursor-pointer underline font-bold" wire:click="takePop({{ $pop['population']['value'] }})">
population: {{ number_format($pop['population']['value']) }}
</div>
@endforeach
</div>
<h1>DB population</h1>
<code>
population: {{ $meetup->city->population }}
</code>
<code>
population date: {{ $meetup->city->population_date }}
</code>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div class="p-6">
<livewire:tables.meetup-for-btc-map-table/>
</div>

View File

@@ -175,8 +175,20 @@ Route::middleware([
'needMeetup',
])
->group(function () {
/*
* Dashboard
* */
Route::get('/dashboard', function () {
return view('dashboard');
})
->name('dashboard');
/*
* Meetup OSM
* */
Route::get('/meetup-osm/table', \App\Http\Livewire\Meetup\PrepareForBtcMapTable::class)
->name('osm.meetups')
->can('NovaAdminPolicy.viewAny');
Route::get('/meetup-osm/item/{meetup}', \App\Http\Livewire\Meetup\PrepareForBtcMapItem::class)
->name('osm.meetups.item')
->can('NovaAdminPolicy.viewAny');
});