From 9208ddf7799e564c7da484e104a0c31fbf30afd8 Mon Sep 17 00:00:00 2001 From: Deon George Date: Sat, 31 Aug 2024 22:23:07 +1000 Subject: [PATCH] Major refactor of photo processing, video processing still to do --- app/Casts/PostgresBytea.php | 35 + app/Console/Commands/CatalogDump.php | 45 - app/Console/Commands/CatalogScan.php | 53 +- app/Console/Commands/CatalogScanAll.php | 145 +- app/Console/Commands/CatalogVerify.php | 50 - app/Console/Commands/PhotoUpdate.php | 107 - app/Http/Controllers/PhotoController.php | 16 +- app/Jobs/CatalogScan.php | 67 +- app/Jobs/CatalogVerify.php | 100 - app/Models/Abstracted/Catalog.php | 593 ++-- app/Models/Photo.php | 321 ++- app/Providers/AppServiceProvider.php | 7 +- app/Traits/Type.php | 2 +- bootstrap/providers.php | 5 + composer.json | 140 +- config/filesystems.php | 128 +- config/photo.php | 2 +- config/video.php | 2 +- public/js/maplibre-style.json | 2560 +++++++++++++++++ .../views/catalog/deletereview.blade.php | 5 +- .../views/catalog/duplicatereview.blade.php | 6 +- .../catalog/widgets/duplicates.blade.php | 24 +- resources/views/components/info.blade.php | 1 + .../views/components/thumbnail.blade.php | 3 + resources/views/photo/view.blade.php | 156 +- .../views/photo/widgets/thumbnail.blade.php | 21 +- routes/web.php | 4 +- 27 files changed, 3581 insertions(+), 1017 deletions(-) create mode 100644 app/Casts/PostgresBytea.php delete mode 100644 app/Console/Commands/CatalogDump.php delete mode 100644 app/Console/Commands/CatalogVerify.php delete mode 100644 app/Console/Commands/PhotoUpdate.php delete mode 100644 app/Jobs/CatalogVerify.php create mode 100644 bootstrap/providers.php create mode 100644 public/js/maplibre-style.json create mode 100644 resources/views/components/info.blade.php create mode 100644 resources/views/components/thumbnail.blade.php diff --git a/app/Casts/PostgresBytea.php b/app/Casts/PostgresBytea.php new file mode 100644 index 0000000..36e7b38 --- /dev/null +++ b/app/Casts/PostgresBytea.php @@ -0,0 +1,35 @@ + $attributes + */ + public function get(Model $model, string $key, mixed $value, array $attributes): mixed + { + // For stream resources, we need to fseek in case we've already read it. + if (is_resource($value)) { + rewind($value); + $value = stream_get_contents($value); + } + + return hex2bin($value); + } + + /** + * Prepare the given value for storage. + * + * @param array $attributes + */ + public function set(Model $model, string $key, mixed $value, array $attributes): mixed + { + return bin2hex($value); + } +} diff --git a/app/Console/Commands/CatalogDump.php b/app/Console/Commands/CatalogDump.php deleted file mode 100644 index c8d3a29..0000000 --- a/app/Console/Commands/CatalogDump.php +++ /dev/null @@ -1,45 +0,0 @@ -getModelType($this->argument('type')); - - $o = $class::findOrFail($this->argument('id')); - - if (! $o->isReadable()) { - $this->warn(sprintf('Ignoring [%s], it is not readable',$o->file_path())); - exit; - } - - dump($o->properties()); - } -} \ No newline at end of file diff --git a/app/Console/Commands/CatalogScan.php b/app/Console/Commands/CatalogScan.php index 6af91c0..6137257 100644 --- a/app/Console/Commands/CatalogScan.php +++ b/app/Console/Commands/CatalogScan.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; +use App\Jobs\CatalogScan as Job; use App\Traits\Type; class CatalogScan extends Command @@ -25,7 +26,7 @@ class CatalogScan extends Command * * @var string */ - protected $description = 'Scan Photo for metadata'; + protected $description = 'Scan Photo/Video for metadata'; /** * Execute the console command. @@ -38,54 +39,6 @@ class CatalogScan extends Command $o = $class::findOrFail($this->argument('id')); - if (! $o->isReadable()) { - $this->warn(sprintf('Ignoring [%s], it is not readable',$o->file_path())); - return; - } - - $o->setDateCreated(); - $o->setSubSecTime(); - $o->setSignature(); - $o->setMakeModel(); - $o->setLocation(); - $o->setHeightWidth(); - $o->setThumbnail(); - - // If this is a duplicate - $x = $o->myduplicates()->get(); - if (count($x)) { - foreach ($x as $oo) { - // And that photo is not marked as a duplicate - if (! $oo->duplicate) { - $o->duplicate = '1'; - $this->warn(sprintf('Image [%s] marked as a duplicate',$o->filename)); - - // If the file signature also matches, we'll mark it for deletion - if ($oo->file_signature AND $o->file_signature == $oo->file_signature) { - $this->warn(sprintf('Image [%s] marked for deletion',$o->filename)); - $o->remove = '1'; - } - - break; - } - } - } - - $o->scanned = '1'; - - if ($o->getDirty()) { - $this->warn(sprintf('Image [%s] metadata changed',$o->filename)); - - if ($this->option('dirty')) - dump(['id'=>$o->id,'data'=>$o->getDirty()]); - } - - // If the file signature changed, abort the update. - if ($o->getOriginal('file_signature') AND $o->wasChanged('file_signature')) { - dump(['old'=>$o->getOriginal('file_signature'),'new'=>$o->file_signature]); - abort(500,'File Signature Changed?'); - } - - $o->save(); + return Job::dispatchSync($o); } } \ No newline at end of file diff --git a/app/Console/Commands/CatalogScanAll.php b/app/Console/Commands/CatalogScanAll.php index 4d3dc2a..df0895b 100644 --- a/app/Console/Commands/CatalogScanAll.php +++ b/app/Console/Commands/CatalogScanAll.php @@ -2,9 +2,12 @@ namespace App\Console\Commands; +use Carbon\Carbon; use Illuminate\Console\Command; use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; use App\Jobs\CatalogScan; use App\Traits\Type; @@ -13,14 +16,18 @@ class CatalogScanAll extends Command { use DispatchesJobs,Type; + private int|bool $depth = true; + protected const chunk_size = 5; + /** * The name and signature of the console command. * * @var string */ - protected $signature = 'catalog:scanall'. - ' {type : Photo | Video }'. - ' {--scanned : Rescan Scanned Photos}'; + protected $signature = 'catalog:scanall' + .' {type : Photo | Video }' + .' {--i|ignore : Ignore missing files}' + .' {--s|scan : Force Rescan of all files }'; /** * The console command description. @@ -30,40 +37,128 @@ class CatalogScanAll extends Command protected $description = '(re)Scan Media'; /** - * Create a new command instance. + * Execute the console command. * - * @return void + * @return int */ - public function __construct() + public function handle(): int { - parent::__construct(); + $started = Carbon::now(); + $class = $this->getModelType($this->argument('type')); + + Log::info('Scanning disk: '.Storage::disk('nas')->path('')); + + $c = 0; + // Scan files in dir, and make sure file lives in DB, (touch it if it does), otherwise create it + foreach (Storage::disk('nas')->directories($class::dir_prefix()) as $dir) { + Log::info(sprintf(' - DIR: %s',$dir)); + + // Take x files at a time and check the DB + foreach ($this->files($dir,$class::config,$class::dir_prefix())->chunk(self::chunk_size) as $chunk) { + $list = $class::whereIn('filename',$chunk)->get(); + + // If there is a new file found it wont be in the DB + if ($list->count() !== self::chunk_size) + foreach ($chunk->diff($list->pluck('filename')) as $file) { + Log::info(sprintf('Found new file [%s] - queueing scan',$file)); + + $o = new $class; + $o->filename = $file; + $o->file_signature = $o->getObjectOriginal('file_signature'); + $o->save(); + + CatalogScan::dispatch($o) + ->onQueue('scan'); + + $c++; + } + + foreach ($list as $o) { + // Check the details are valid + if ($o->file_signature === $o->getObjectOriginal('file_signature')) { + // For sanity, we'll check a couple of other attrs + if (($o->width !== $o->getObjectOriginal('width')) || ($o->height !== $o->getObjectOriginal('height'))) + Log::alert(sprintf('Dimensions [%s] (%s x %s) mismatch for [%s]', + $o->dimensions, + $o->getObjectOriginal('width'), + $o->getObjectOriginal('height'), + $o->file_name(FALSE))); + + } else { + Log::alert(sprintf('File Signature [%s] doesnt match [%s] for [%s]', + $o->getObjectOriginal('file_signature'), + $o->file_signature, + $o->file_name(FALSE))); + } + + if ($o->signature !== $o->getObjectOriginal('signature')) { + Log::notice(sprintf('Updating image signature for [%s] to [%s] was [%s]',$o->filename,$o->signature,$o->getObjectOriginal('signature'))); + + $o->signature = $o->getObjectOriginal('signature'); + } + + if ($o->isDirty()) + $o->save(); + else + $o->touch(); + + if ($this->option('scan')) { + Log::info(sprintf('Forcing re-scan of [%s] - queued',$o->filename)); + + CatalogScan::dispatch($o) + ->onQueue('scan'); + } + + $c++; + } + } + + break; + } + + Log::info('Checking for missing files'); + + // Find DB records before $started, check they exist (they shouldnt), and delete if not + if (! $this->option('ignore')) + foreach ($class::select(['id','filename'])->where('updated_at','<',$started)->cursor() as $o) + Log::error(sprintf('It appears that file [%s] is missing (%d)',$o->filename,$o->id)); + + Log::info(sprintf('Processed [%s]',$c)); + + return self::SUCCESS; } /** - * Execute the console command. + * Recursively find files that we should catalog * - * @return mixed + * @param string $dir Directory to get files from + * @param string $type Configuration key to refer to config()) + * @param string $prefix Remove the prefix from the filename + * @return Collection */ - public function handle() + public function files(string $dir,string $type,string $prefix): Collection { - $class = $this->getModelType($this->argument('type')); + $files = collect(Storage::disk('nas')->files($dir)) + ->map(fn($item)=>preg_replace('#^'.$prefix.'#','',$item)) + ->filter(function($item) use ($type) { + return ((! ($x=strrpos($item,'.'))) + || (! in_array(strtolower(substr($item,$x+1)),config($type.'.import.accepted')))) + ? NULL + : $item; + }); - if ($this->option('scanned')) { - $class::whereNotNull('scanned') - ->update(['scanned'=>NULL]); - } + if (! $this->depth) + return $files; - $c = 0; - $class::NotScanned()->each(function ($item) use ($c) { - if ($item->remove) { - Log::warning(sprintf('Not scanning [%s], marked for removal',$item->id)); - return; - } + if (is_numeric($this->depth)) + $this->depth--; - $this->dispatch((new CatalogScan($item))->onQueue('scan')); - $c++; - }); + foreach (Storage::disk('nas')->directories($dir) as $dir) + $files = $files->merge($this->files($dir,$type,$prefix)); - Log::info(sprintf('Processed [%s]',$c)); + if (is_numeric($this->depth)) + $this->depth++; + + return $files; } } \ No newline at end of file diff --git a/app/Console/Commands/CatalogVerify.php b/app/Console/Commands/CatalogVerify.php deleted file mode 100644 index 5d8b56d..0000000 --- a/app/Console/Commands/CatalogVerify.php +++ /dev/null @@ -1,50 +0,0 @@ -argument('type')) { - Job::dispatch($this->argument('type'))->onQueue('scan'); - - } else { - } - } -} \ No newline at end of file diff --git a/app/Console/Commands/PhotoUpdate.php b/app/Console/Commands/PhotoUpdate.php deleted file mode 100644 index dbbf7de..0000000 --- a/app/Console/Commands/PhotoUpdate.php +++ /dev/null @@ -1,107 +0,0 @@ -getFiles(['dir'=>$this->option('dir'),'file'=>$this->option('file')]); - if (! count($files)) - exit; - - // Show a progress bar - $bar = $this->output->createProgressBar(count($files)); - $bar->setFormat("%current%/%max% [%bar%] %percent:3s%% (%memory%) (%remaining%) "); - - $c = 0; - foreach ($files as $file) - { - $bar->advance(); - - if (preg_match('/@__thumb/',$file) OR preg_match('/\/._/',$file)) - { - $this->warn(sprintf('Ignoring file [%s]',$file)); - continue; - } - - if (! in_array(strtolower(pathinfo($file,PATHINFO_EXTENSION)),config('photo.import.accepted'))) - { - $this->warn(sprintf('Ignoring [%s]',$file)); - continue; - } - - if ($this->option('verbose')) - $this->info(sprintf('Processing file [%s]',$file)); - - $c++; - - $po = Photo::where('filename',$file)->first(); - - if (is_null($po)) - { - $this->error(sprintf('File is not in the database [%s]?',$file)); - Log::error(sprintf('%s: File is not in the database [%s]?',__METHOD__,$file)); - continue; - } - - $po->signature = $po->property('signature'); - - try { - $po->thumbnail = exif_thumbnail($po->file_path()); - } catch (\Exception $e) { - // @todo Couldnt get the thumbnail, so we should create one. - } - - if ($po->isDirty()) - { - if (count($po->getDirty()) > 1 OR ! array_key_exists('signature',$po->getDirty())) - $this->error(sprintf('More than the signature changed for [%s] (%s)?',$po->id,join('|',array_keys($po->getDirty())))); - - $this->info(sprintf('Signature update for [%s]',$po->id)); - $po->save(); - } - } - - $bar->finish(); - - return $this->info(sprintf('Images processed: %s',$c)); - } -} diff --git a/app/Http/Controllers/PhotoController.php b/app/Http/Controllers/PhotoController.php index a7e12b1..51bc3cb 100644 --- a/app/Http/Controllers/PhotoController.php +++ b/app/Http/Controllers/PhotoController.php @@ -38,14 +38,9 @@ class PhotoController extends Controller ]); } - public function info(Photo $o) - { - return view('photo.view',['o'=>$o]); - } - public function thumbnail(Photo $o) { - return response($o->thumbnail(TRUE)) + return response($o->thumbnail()) ->header('Content-Type','image/jpeg'); } @@ -54,9 +49,16 @@ class PhotoController extends Controller $o->remove = NULL; $o->save(); - return redirect()->action('PhotoController@info',[$o->id]); + return redirect() + ->action('PhotoController@info',[$o->id]); } + /** + * Render the photo to the browser + * + * @param Photo $o + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Foundation\Application|\Illuminate\Http\Response + */ public function view(Photo $o) { return response($o->image()) diff --git a/app/Jobs/CatalogScan.php b/app/Jobs/CatalogScan.php index d583a69..e7fd562 100644 --- a/app/Jobs/CatalogScan.php +++ b/app/Jobs/CatalogScan.php @@ -2,39 +2,92 @@ namespace App\Jobs; +use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Artisan; use App\Models\Abstracted\Catalog; -class CatalogScan extends Job implements ShouldQueue +class CatalogScan implements ShouldQueue, ShouldBeUnique { - use InteractsWithQueue, SerializesModels; + use InteractsWithQueue,Queueable,SerializesModels; // Our object - private $o = NULL; + private Catalog $o; + private bool $show_dirty; /** * Create a new job instance. * * @return void */ - public function __construct(Catalog $o) { - $this->o = $o; + public function __construct(Catalog $o,bool $show_dirty=FALSE) { + $this->o = $o->withoutRelations(); + $this->show_dirty = $show_dirty; + } + + public function xmiddleware(): array + { + return [new WithoutOverlapping($this->o->id)]; } /** * Execute the job. * * @return void + * @throws \Exception */ public function handle() { Log::info(sprintf('%s: Scanning [%s|%s]',__METHOD__,$this->o->objecttype(),$this->o->id)); - Artisan::call('catalog:scan',['type'=>$this->o->objecttype(),'id'=>$this->o->id]); + if (! $this->o->isReadable()) { + Log::alert(sprintf('Ignoring [%s], it is not readable',$this->o->file_name(FALSE))); + + return; + } + + $this->o->init(); + + // If this is a duplicate + $x = $this->o->myduplicates()->get(); + if (count($x)) { + foreach ($x as $this->oo) { + // And that photo is not marked as a duplicate + if (! $this->oo->duplicate) { + $this->o->duplicate = TRUE; + Log::alert(sprintf('Image [%s] marked as a duplicate',$this->o->filename)); + + // If the file signature also matches, we'll mark it for deletion + if ($this->oo->file_signature && ($this->o->file_signature == $this->oo->file_signature)) { + Log::alert(sprintf('Image [%s] marked for deletion',$this->o->filename)); + $this->o->remove = TRUE; + } + + break; + } + } + } + + $this->o->scanned = TRUE; + + if ($this->o->getDirty()) { + Log::alert(sprintf('Image [%s] metadata changed',$this->o->filename)); + + if ($this->show_dirty) + dump(['id'=>$this->o->id,'data'=>$this->o->getDirty()]); + } + + // If the file signature changed, abort the update. + if ($this->o->getOriginal('file_signature') && $this->o->wasChanged('file_signature')) { + dump(['old'=>$this->o->getOriginal('file_signature'),'new'=>$this->o->file_signature]); + abort(500,'File Signature Changed?'); + } + + $this->o->save(); } } \ No newline at end of file diff --git a/app/Jobs/CatalogVerify.php b/app/Jobs/CatalogVerify.php deleted file mode 100644 index de93ad3..0000000 --- a/app/Jobs/CatalogVerify.php +++ /dev/null @@ -1,100 +0,0 @@ -type = $type; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - Log::info(sprintf('%s: Scanning [%s]',__METHOD__,$this->type)); - - // Go through DB and verify files exist - $class = $this->getModelType($this->type); - - $good = $bad = $ugly = 0; - - $class::select('*')->each(function($o) use ($good,$bad,$ugly) { - if (! file_exists($o->file_name_current(FALSE))) { - Log::error(sprintf('Media doesnt exist: [%s] (%d:%s)',$this->type,$o->id,$o->file_name_current(FALSE))); - $bad++; - return; - } - - if (($x=md5_file($o->file_name_current(FALSE))) !== $o->file_signature) { - Log::error(sprintf('Media MD5 doesnt match DB: [%s] (%d:%s) [%s:%s]',$this->type,$o->id,$o->file_name_current(FALSE),$x,$o->file_signature)); - $ugly++; - return; - } - - $good++; - }); - - Log::info(sprintf('DB Media Verify Complete: [%s] Good: [%d], Missing: [%d], Changed: [%d]',$this->type,$good,$bad,$ugly)); - - // Go through filesystem and see that a record exists in the DB, if not add it. - $parentdir = config($this->type.'.dir'); - - $good = $bad = 0; - - foreach ($this->dirlist($parentdir) as $dir) { - foreach ($this->getFiles(['dir'=>$dir,'file'=>NULL],$this->type) as $file) { - if (! $class::where('filename',$file)->count()) { - $bad++; - Log::error(sprintf('File not in DB: [%s] (%s)',$this->type,$file)); - } else { - $good++; - } - } - } - - Log::info(sprintf('File Media Verify Complete: [%s] Good: [%d], Not In DB: [%d]',$this->type,$good,$bad)); - } - - /** - * Recursively get a list of dirs - * @param $path - * @return \Illuminate\Support\Collection - */ - private function dirlist($path) - { - $list = collect(); - - $list->push($path); - - foreach (glob($path.'/*',GLOB_ONLYDIR) as $dir) { - foreach ($this->dirlist($dir) as $subdir) - $list->push($subdir); - } - - return $list; - } -} \ No newline at end of file diff --git a/app/Models/Abstracted/Catalog.php b/app/Models/Abstracted/Catalog.php index 54981eb..4e2e3f2 100644 --- a/app/Models/Abstracted/Catalog.php +++ b/app/Models/Abstracted/Catalog.php @@ -5,20 +5,56 @@ namespace App\Models\Abstracted; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; -use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; -use App\Models\{Person,Software,Tag}; +use App\Casts\PostgresBytea; +use App\Models\{Make,Person,Software,Tag}; abstract class Catalog extends Model { protected static $includeSubSecTime = FALSE; - protected $dates = ['created','created_manual']; protected $casts = [ + 'created_manual' => 'datetime', 'subsectime' => 'int', + 'thumbnail' => PostgresBytea::class, ]; + protected const fs = 'nas'; + + private ?string $move_reason; + + protected array $init = []; + + /* STATIC */ + + /** + * Return the prefix for the file path - dependant on the object + * + * @return string + */ + public static function dir_prefix(): string + { + return config(static::config.'.dir').'/'; + } + + /** + * Trim a string + * + * @param string $string + * @param int $chrs + * @return string + * @todo This should go in as a helper + */ + public static function stringtrim(string $string,int $chrs=6) + { + return sprintf('%s...%s',substr($string,0,$chrs),substr($string,-1*$chrs)); + } + + /* RELATIONS */ + /** * People in Multimedia Object * @@ -49,19 +85,19 @@ abstract class Catalog extends Model return $this->belongsToMany(Tag::class); } + /* SCOPES */ + /** * Find records marked as duplicate * * @param $query * @return mixed */ - public function scopeDuplicates($query) { - $query->notRemove() + public function scopeDuplicates($query) + { + return $query->notRemove() ->where('duplicate',TRUE) - ->where(function($q) { - $q->Where('ignore_duplicate','<>',TRUE) - ->orWhereNull('ignore_duplicate'); - }); + ->where(fn($q)=>$q->where('ignore_duplicate','<>',TRUE)->orWhereNull('ignore_duplicate')); } /** @@ -78,29 +114,20 @@ abstract class Catalog extends Model $query->where('id','<>',$this->attributes['id']); // Skip ignore dups - $query->where(function($q) { - $q->whereNull('ignore_duplicate') - ->orWhere('ignore_duplicate','=',0); - }); + $query->where(fn($q)=>$q->whereNull('ignore_duplicate')->orWhere('ignore_duplicate',FALSE)); // Exclude those marked as remove - $query->where(function ($q) { - $q->where('remove','<>',TRUE) - ->orWhere('remove','=',NULL); - }); + $query->where(fn($q)=>$q->where('remove','<>',TRUE)); - $query->where(function ($q) { - $q->where('signature','=',$this->attributes['signature']) + $query->where(function($q) { + $q->when($this->attributes['signature'],fn($q)=>$q->where('signature','=',$this->attributes['signature'])) ->orWhere('file_signature','=',$this->attributes['file_signature']) // Where the signature is the same ->orWhere(function($q) { // Or they have the same time taken with the same camera if ($this->attributes['created'] AND $this->software_id) { - $q->where(function ($q) { - $q->where('created','=',$this->attributes['created']) - ->orWhere('created_manual','=',$this->attributes['created']); - }); + $q->where(fn($q)=>$q->where('created','=',$this->attributes['created'])->orWhere('created_manual','=',$this->attributes['created'])); if (static::$includeSubSecTime) $q->where('subsectime','=',Arr::get($this->attributes,'subsectime')); @@ -108,10 +135,7 @@ abstract class Catalog extends Model $q->where('software_id','=',$this->attributes['software_id']); } elseif ($this->attributes['created_manual'] AND $this->software_id) { - $q->where(function ($q) { - $q->where('created','=',$this->attributes['created_manual']) - ->orWhere('created_manual','=',$this->attributes['created_manual']); - }); + $q->where(fn($q)=>$q->where('created','=',$this->attributes['created_manual'])->orWhere('created_manual','=',$this->attributes['created_manual'])); if (static::$includeSubSecTime) $q->where('subsectime','=',Arr::get($this->attributes,'subsectime')); @@ -120,6 +144,8 @@ abstract class Catalog extends Model } }); }); + + return $query; } /** @@ -129,15 +155,11 @@ abstract class Catalog extends Model */ public function scopeNotDuplicate($query) { - return $query->where(function($query) - { - $query->where('duplicate','<>',TRUE) + return $query->where( + fn($q)=>$q->where('duplicate','<>',TRUE) ->orWhere('duplicate','=',NULL) - ->orWhere(function($q) { - $q->where('duplicate','=',TRUE) - ->where('ignore_duplicate','=',TRUE); - }); - }); + ->orWhere(fn($q)=>$q->where('duplicate','=',TRUE)->where('ignore_duplicate','=',TRUE)) + ); } /** @@ -147,11 +169,7 @@ abstract class Catalog extends Model */ public function scopeNotRemove($query) { - return $query->where(function($query) - { - $query->where('remove','<>',TRUE) - ->orWhere('remove','=',NULL); - }); + return $query->where(fn($q)=>$q->where('remove','<>',TRUE)->orWhere('remove','=',NULL)); } /** @@ -161,27 +179,33 @@ abstract class Catalog extends Model */ public function scopeNotScanned($query) { - return $query->where(function($query) - { - $query->where('scanned','<>',TRUE) - ->orWhere('scanned','=',NULL); - }); + return $query->where(fn($q)=>$q->where('scanned','<>',TRUE)->orWhere('scanned','=',NULL)); } - // Children objects must inherit this methods - abstract public function setLocation(); - abstract public function setSubSecTime(); - abstract public function setThumbnail(); - abstract public function getHtmlImageURL(); + /* ABSTRACTS */ + + abstract public function getObjectOriginal(string $property): mixed; + + /* ATTRIBUTES */ /** - * Date the multimedia was created + * Return the time the media was created on the device + * + * This will be (in priority order) + * + the value of created_manual (Carbon) + * + the value of created + * + * @param string|null $date + * @return Carbon|null */ - public function date_taken(): string + public function getCreatedAttribute(string $date=NULL): ?Carbon { - return $this->created - ? $this->created->format('Y-m-d H:i:s').(static::$includeSubSecTime ? sprintf('.%03d',$this->subsectime) : '') - : 'UNKNOWN'; + $result = $this->created_manual ?: ($date ? Carbon::create($date) : NULL); + + if ($result && static::$includeSubSecTime) + $result->microseconds($this->subsectime*1000); + + return $result ?: $this->getObjectOriginal('creation_date'); } /** @@ -189,7 +213,7 @@ abstract class Catalog extends Model * * @return string */ - public function device(): string + public function getDeviceAttribute(): string { $result = ''; @@ -209,61 +233,105 @@ abstract class Catalog extends Model } /** - * Return the date of the file - * @todo return Carbon date or NULL + * Return item dimensions */ - public function file_date($type,$format=FALSE) + public function getDimensionsAttribute(): string { - if (! is_readable($this->file_path())) - return NULL; - - switch ($type) - { - case 'a': $t = fileatime($this->file_path()); - break; - - case 'c': $t = filectime($this->file_path()); - break; - - case 'm': $t = filemtime($this->file_path()); - break; - } - - return $format ? date('d-m-Y H:i:s',$t) : $t; + return $this->width.'x'.$this->height; } /** - * Return what the filename should be. + * Return the file size * + * @return int|null + */ + public function getFileSizeAttribute(): ?int + { + return (! $this->isReadable()) ? NULL : filesize($this->file_name(FALSE)); + } + + public function getGPSAttribute(): ?string + { + return ($this->gps_lat && $this->gps_lon) + ? sprintf('%s/%s',$this->gps_lat,$this->gps_lon) + : NULL; + } + + /* METHODS */ + + /** + * Date the multimedia was created + * + * @deprecated use $this->created + */ + public function date_taken(): string + { + Log::alert(__METHOD__.' deprecated'); + return $this->created; + } + + /** @deprecated use $this->device */ + public function device(): string + { + Log::alert(__METHOD__.' deprecated'); + return $this->device; + } + + /** + * Return the date of the file + */ + public function file_date(string $type): ?Carbon + { + if (! $this->isReadable()) + return NULL; + + $t = NULL; + + switch ($type) { + case 'a': $t = fileatime($this->file_name(FALSE)); + break; + + case 'c': $t = filectime($this->file_name(FALSE)); + break; + + case 'm': $t = filemtime($this->file_name(FALSE)); + break; + } + + return $t ? Carbon::createfromTimestamp($t) : NULL; + } + + /** + * Return the filename. + * If short is TRUE, it is the filename that it should be called (and can be compared to $this->filename) + * If short is FALSE, it is the true path of the actual file + * + * @param bool $short * @return string */ public function file_name(bool $short=TRUE): string { - // If the date created is not set, the file name will be based on the ID of the file. - $file = sprintf('%s.%s',(is_null($this->created) - ? sprintf('UNKNOWN/%07s',$this->file_path_id()) - : $this->created->format('Y/m/d-His'). - ((! is_null($this->subsectime)) ? sprintf('_%03d',$this->subsectime) : '' ). - sprintf('-%05s',$this->id)) - ,$this->type() - ); + if ($short || preg_match('#^/#',$this->filename)) { + // If the date created is not set, the file name will be based on the ID of the file. + $file = sprintf('%s.%s', + (is_null($this->created) + ? sprintf('UNKNOWN/%07s',$this->file_path_id()) + : $this->created->format('Y/m/d-His'). + ($this->subsectime ? sprintf('_%03d',$this->subsectime) : '' ). + sprintf('-%05s',$this->id)), + $this->type() + ); - return (($short OR preg_match('/^\//',$file)) ? '' : config($this->type.'.dir').DIRECTORY_SEPARATOR).$file; - } + return $file; - /** - * Return the current filename - * - * @return string - */ - public function file_name_current(bool $short=TRUE): string - { - return (($short OR preg_match('/^\//',$this->filename)) ? '' : config($this->type.'.dir').DIRECTORY_SEPARATOR).$this->filename; + } else + return Storage::disk(self::fs) + ->path(config(static::config.'.dir').DIRECTORY_SEPARATOR.$this->filename); } /** * Determine the new name for the image - * @deprecated use $this->file_name() + * @deprecated use $this->file_name(FALSE) to determine the name, and file_name(TRUE) to determine the new name */ public function file_path($short=FALSE,$new=FALSE) { @@ -277,62 +345,37 @@ abstract class Catalog extends Model /** * Calculate a file path ID based on the id of the file + * + * We use this when we cannot determine the create time of the image */ public function file_path_id($sep=3,$depth=9): string { return trim(chunk_split(sprintf("%0{$depth}s",$this->id),$sep,'/'),'/'); } - /** - * Display the file signature - */ - public function file_signature($short=FALSE): string - { - return $short ? static::stringtrim($this->file_signature) : $this->file_signature; - } - /** * Return the file size - * @deprecated + * @deprecated use $this->getFileSizeAttribute()) */ public function file_size() { - return (! is_readable($this->file_path())) ? NULL : filesize($this->file_path()); - } + Log::alert(__METHOD__.' deprecated'); - public function getCreatedAttribute() - { - return $this->created_manual ?: ($this->attributes['created'] ? $this->asDateTime($this->attributes['created']) : NULL); - } - - /** - * Return item dimensions - */ - public function getDimensionsAttribute(): string - { - return $this->width.'x'.$this->height; + return (! is_readable($this->file_name(FALSE))) ? NULL : filesize($this->file_name(FALSE)); } /** * Return HTML Checkbox for duplicate + * @deprecated use a component */ public function getDuplicateCheckboxAttribute() { return $this->HTMLCheckbox('duplicate',$this->id,$this->duplicate); } - /** - * Return the file size - * - * @return false|int|null - */ - public function getFileSizeAttribute() - { - return (! is_readable($this->file_path())) ? NULL : filesize($this->file_path()); - } - /** * Return HTML Checkbox for flagged + * @deprecated use a component */ public function getFlagCheckboxAttribute() { @@ -341,6 +384,7 @@ abstract class Catalog extends Model /** * Return HTML Checkbox for ignore + * @deprecated use a component */ public function getIgnoreCheckboxAttribute() { @@ -348,7 +392,7 @@ abstract class Catalog extends Model } /** - * @deprecated + * @deprecated use a component */ public function getDuplicateTextAttribute() { @@ -356,7 +400,7 @@ abstract class Catalog extends Model } /** - * @deprecated + * @deprecated use a component */ public function getFlagTextAttribute() { @@ -365,32 +409,167 @@ abstract class Catalog extends Model /** * Return HTML Checkbox for remove + * @deprecated use a component */ public function getRemoveCheckboxAttribute() { return $this->HTMLCheckbox('remove',$this->id,$this->remove); } - /** - * Return the object type - * @return string - * @deprecated Use objecttype() - */ - public function getTypeAttribute(): string - { - switch(get_class($this)) { - case 'App\Models\Photo': return 'photo'; - case 'App\Models\Video': return 'video'; - default: return 'unknown'; - } - } - /** * Display the GPS coordinates + * @deprecated use getGPSAttribute() */ public function gps(): string { - return ($this->gps_lat AND $this->gps_lon) ? sprintf('%s/%s',$this->gps_lat,$this->gps_lon) : 'UNKNOWN'; + return $this->getGPSAttribute(); + } + + /** + * Return an HTML checkbox + * @deprecated use a component + */ + protected function HTMLCheckbox($name,$id,$value) + { + return sprintf('',$name,$id,$value ? ' checked="checked"' : ''); + } + + /** + * Get ID Info link + * @deprecated use a component + */ + protected function HTMLLinkAttribute($id,$url) + { + return sprintf('%s',url($url,$id),$id,$id); + } + + /** + * Set values from the media object + * + * @return void + * @throws \Exception + */ + public function init(): void + { + foreach ($this->init as $item) { + Log::debug(sprintf('Init item [%s]',$item)); + + switch ($item) { + case 'creation_date': + $this->created = $this->getObjectOriginal('creation_date'); + break; + + case 'gps': + $this->gps_lat = $this->getObjectOriginal('gps_lat'); + $this->gps_lon = $this->getObjectOriginal('gps_lon'); + break; + + case 'heightwidth': + $this->height = $this->getObjectOriginal('height'); + $this->width = $this->getObjectOriginal('width'); + break; + + case 'signature': + $this->signature = $this->getObjectOriginal('signature'); + $this->file_signature = $this->getObjectOriginal('file_signature'); + break; + + case 'software': + $ma = NULL; + + if ($x=$this->getObjectOriginal('make')) + $ma = Make::firstOrCreate([ + 'name'=>$x, + ]); + + $mo = \App\Models\Model::firstOrCreate([ + 'name'=>$this->getObjectOriginal('model') ?: NULL, + 'make_id'=>$ma?->id, + ]); + + $so = Software::firstOrCreate([ + 'name'=>$this->getObjectOriginal('software') ?: NULL, + 'model_id'=>$mo->id, + ]); + + $this->software_id = $so->id; + + case 'subsectime': + $this->subsectime = $this->getObjectOriginal($item); + break; + + default: + throw new \Exception('Unknown init item: '.$item); + } + } + + $this->custom_init(); + + Log::debug('Init result',['dirty'=>$this->getDirty()]); + } + + /** + * Does the file require moving + * + * @return bool + */ + public function isMoveable(): bool + { + // No change to the name + $this->move_reason = 'Filenames match already'; + if ($this->filename === $this->file_name()) + return FALSE; + + // If there is already a file in the target. + // @todo If the target file is to be deleted, we could move this file + $this->move_reason = 'Target file exists'; + if (Storage::disk(self::fs)->exists($this->file_name())) + return FALSE; + + // Test if the source is readable + $this->move_reason = 'Source is not readable'; + if (! $this->isReadable()) + return FALSE; + + // Test if the dir is writable (so we can remove the file) + $this->move_reason = 'Source parent dir not writable'; + if (! $this->isParentWritable(dirname($this->file_name(FALSE)))) + return FALSE; + + // Test if the target dir is writable + // @todo The target dir may not exist yet, so we should check that a parent exists and is writable. + $this->move_reason = 'Target parent dir doesnt is not writable'; + if (! $this->isParentWritable(dirname($this->file_name(FALSE)))) + return FALSE; + + // Otherwise we can move it + $this->move_reason = NULL; + return TRUE; + } + + public function isMoveableReason(): ?string + { + return isset($this->move_reason) ? $this->move_reason : NULL; + } + + /** + * Determine if the parent dir is writable + * + * @param string $dir + * @return bool + */ + public function isParentWritable(string $dir): bool + { + $path = Storage::disk(self::fs)->path($dir); + + if (Storage::disk(self::fs)->exists($dir) && is_dir($path) && is_writable($path)) + return TRUE; + + elseif ($path === dirname($path)) + return FALSE; + + else + return ($this->isParentWritable(dirname($dir))); } /** @@ -400,41 +579,7 @@ abstract class Catalog extends Model */ public function isReadable(): bool { - return is_readable($this->file_path()); - } - - /** - * Return an HTML checkbox - */ - protected function HTMLCheckbox($name,$id,$value) - { - return sprintf('',$name,$id,$value ? ' checked="checked"' : ''); - } - - /** - * Get ID Info link - */ - protected function HTMLLinkAttribute($id,$url) - { - return sprintf('%s',url($url,$id),$id,$id); - } - - /** - * Determine if the parent dir is writable - * - * @param $dir - * @return bool - */ - public function isParentWritable($dir): bool - { - if (file_exists($dir) AND is_writable($dir) AND is_dir($dir)) - return TRUE; - - elseif ($dir == dirname($dir) OR file_exists($dir)) - return FALSE; - - else - return ($this->isParentWritable(dirname($dir))); + return is_readable($this->file_name(FALSE)); } /** @@ -445,44 +590,20 @@ abstract class Catalog extends Model */ public function moveable() { - // If the source and target are the same, we dont need to move it - if ($this->file_path() == $this->file_path(FALSE,TRUE)) - return FALSE; - - // If there is already a file in the target. - // @todo If the target file is to be deleted, we could move this file - if (file_exists($this->file_path(FALSE,TRUE))) - return 1; - - // Test if the source is readable - if (! is_readable($this->file_path())) - return 2; - - // Test if the dir is writable (so we can remove the file) - if (! $this->isParentWritable(dirname($this->file_path()))) - return 3; - - // Test if the target dir is writable - // @todo The target dir may not exist yet, so we should check that a parent exists and is writable. - if (! $this->isParentWritable($this->file_path(FALSE,TRUE))) - return 4; - - return TRUE; + Log::alert(__METHOD__.' deprecated'); + return $this->isMoveable(); } /** - * Get the id of the previous record + * Get the id of the next record */ - public function next() + public function next(): ?self { - return DB::table($this->getTable()) - ->where('id','>',$this->id) + return static::where('id','>',$this->id) ->orderby('id','ASC') ->first(); } - abstract public function property(string $property); - /** * Return my class shortname */ @@ -494,62 +615,21 @@ abstract class Catalog extends Model /** * Get the id of the previous record */ - public function previous() + public function previous(): ?self { - return DB::table($this->getTable()) - ->where('id','<',$this->id) + return static::where('id','<',$this->id) ->orderby('id','DESC') ->first(); } - public function setDateCreated() - { - $this->created = $this->property('creationdate') ?: NULL; - } - - public function setHeightWidth() - { - $this->height = $this->property('height'); - $this->width = $this->property('width'); - $this->orientation = $this->property('orientation'); - } - - public function setSignature() - { - $this->signature = $this->property('signature'); - - $this->file_signature = md5_file($this->file_path()); - } - /** * Display the media signature */ public function signature($short=FALSE) { - return ($short AND $this->signature) ? static::stringtrim($this->signature) : $this->signature; - } - - /** - * Trim a string - * - * @param string $string - * @param int $chrs - * @return string - */ - public static function stringtrim(string $string,int $chrs=6) - { - return sprintf('%s...%s',substr($string,0,$chrs),substr($string,-1*$chrs)); - } - - /** - * @deprecated - * @param string $string - * @param int $chrs - * @return string - */ - public static function signaturetrim(string $string,int $chrs=6) - { - return static::stringtrim($string,$chrs); + return ($short && $this->signature) + ? static::stringtrim($this->signature) + : $this->signature; } /** @@ -560,7 +640,8 @@ abstract class Catalog extends Model return $this->filename !== $this->file_name(); } - protected function TextTrueFalse($value): string + /** @deprecated is this really needed? */ + private function TextTrueFalse($value): string { return $value ? 'TRUE' : 'FALSE'; } @@ -595,7 +676,7 @@ abstract class Catalog extends Model ->orWhere('remove','=',NULL); }); - // Where the signature is the same + // Where the signalist_duplicatesture is the same $o->where(function($query) { $query->where('signature','=',$this->signature); diff --git a/app/Models/Photo.php b/app/Models/Photo.php index cb4e271..6e67aec 100644 --- a/app/Models/Photo.php +++ b/app/Models/Photo.php @@ -2,54 +2,43 @@ namespace App\Models; +use Carbon\Carbon; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Traits\ForwardsCalls; +use Imagick; class Photo extends Abstracted\Catalog { - protected $table = 'photo'; + use ForwardsCalls; + + public const config = 'photo'; protected static $includeSubSecTime = TRUE; - // Imagick Object - private $_o; + // Imagick Objectfile_name + private ?Imagick $_o; + protected array $init = [ + 'creation_date', + 'gps', + 'heightwidth', + 'signature', + 'software', + 'subsectime', + ]; // How should the image be rotated, based on the value of orientation - private $_rotate = [ + private array $_rotate = [ 3=>180, 6=>90, 8=>-90, ]; - public function getIDLinkAttribute() - { - return $this->HTMLLinkAttribute($this->id,'p/info'); - } - - public function getHtmlImageURL(): string - { - return sprintf('',url('p/thumbnail',$this->id)); - } - - /** - * Return the image, rotated, minus exif data - */ - public function image() - { - if (is_null($imo = $this->o())) - return NULL; - - if (array_key_exists('exif',$imo->getImageProfiles())) - $imo->removeImageProfile('exif'); - - return $this->rotate($imo); - } - /** * Calculate the GPS coordinates */ - public static function latlon(array $coordinate,$hemisphere) + public static function latlon(array $coordinate,string $hemisphere): ?float { - if (! $coordinate OR ! $hemisphere) + if ((! $coordinate) || (! $hemisphere)) return NULL; for ($i=0; $i<3; $i++) { @@ -67,29 +56,172 @@ class Photo extends Abstracted\Catalog list($degrees,$minutes,$seconds) = $coordinate; - $sign = ($hemisphere == 'W' || $hemisphere == 'S') ? -1 : 1; + $sign = ($hemisphere === 'W' || $hemisphere === 'S') ? -1 : 1; return round($sign*($degrees+$minutes/60+$seconds/3600),($degrees>100 ? 3 : 4)); } /** - * Return an Imagick object or attribute + * Forward calls to Imagick + * + * @param $method + * @param $parameters + * @return mixed|null */ - protected function o($attr=NULL) + public function __call($method,$parameters) { + if (str_starts_with($method,'Imagick_')) { + $method = preg_replace('/^Imagick_/','',$method); + + return $this->o ? $this->forwardCallTo($this->_o,$method,$parameters) : NULL; + + } else + return parent::__call($method,$parameters); + } + + public function __get($key): mixed { - if (! file_exists($this->file_path()) OR ! is_readable($this->file_path())) - return NULL; + if ($key === 'o') { + if (isset($this->_o)) + return $this->_o; - if (is_null($this->_o)) - $this->_o = new \Imagick($this->file_path()); + if ((!file_exists($this->file_name(FALSE))) || (!is_readable($this->file_name(FALSE)))) + return $this->_o = NULL; - return is_null($attr) ? $this->_o : $this->_o->getImageProperty($attr); + if (!isset($this->_o)) + return $this->_o = new Imagick($this->file_name(FALSE)); + } + + return parent::__get($key); + } + + /* ATTRIBUTES */ + + public function getFileSignatureAttribute(string $val=NULL): string + { + return $val ?: $this->getObjectOriginal('file_signature'); + } + + public function getHeightAttribute(string $val=NULL): ?int + { + return $val ?: $this->getObjectOriginal('height'); + } + + public function getOrientationAttribute(int $val=NULL): ?int + { + return $val ?: $this->getObjectOriginal('orientation'); + } + + public function getSignatureAttribute(string $val=NULL): ?string + { + return $val ?: $this->getObjectOriginal('signature'); + } + + public function getWidthAttribute(string $val=NULL): ?int + { + return $val ?: $this->getObjectOriginal('width'); + } + + /* METHODS */ + + public function custom_init(): void + { + $this->orientation = $this->getObjectOriginal('orientation'); + + try { + if ($this->isReadable() && $this->o->thumbnailimage(150,150,true)) { + $this->o->setImageFormat('jpg'); + + $this->thumbnail = $this->o->getImageBlob(); + } + + } catch (\Exception $e) { + Log::info(sprintf('Unable to create thumbnail for %s (%s)',$this->id,$e->getMessage())); + } + + if ($this->thumbnail === FALSE) + $this->thumbnail = NULL; + } + + public function getObjectOriginal(string $property): mixed + { + switch ($property) { + case 'creation_date': + if ($this->Imagick_getImageProperty('exif:DateTimeOriginal') === '0000:00:00 00:00:00' + && $this->Imagick_getImageProperty('exif:DateTime') === '0000:00:00 00:00:00') + return NULL; + + $result = Carbon::create($x= + ($this->Imagick_getImageProperty('exif:DateTimeOriginal') && ($this->Imagick_getImageProperty('exif:DateTimeOriginal') !== '0000:00:00 00:00:00')) + ? $this->Imagick_getImageProperty('exif:DateTimeOriginal').$this->Imagick_getImageProperty('exif:OffsetTimeOriginal') + : $this->Imagick_getImageProperty('exif:DateTime').$this->Imagick_getImageProperty('exif:OffsetTime')); + + return $result ?: NULL; + + case 'file_signature': + return md5_file($this->file_name(FALSE)); + + case 'gps_lat': + return self::latlon(preg_split('/,\s?/',$this->Imagick_getImageProperty('exif:GPSLatitude')),$this->Imagick_getImageProperty('exif:GPSLatitudeRef')); + + case 'gps_lon': + return self::latlon(preg_split('/,\s?/',$this->Imagick_getImageProperty('exif:GPSLongitude')),$this->Imagick_getImageProperty('exif:GPSLongitudeRef')); + + case 'height': + return $this->Imagick_getImageHeight(); + + case 'identifier': + return NULL; + + case 'make': + return $this->Imagick_getImageProperty('exif:Make'); + + case 'model': + return $this->Imagick_getImageProperty('exif:Model'); + + case 'orientation': + return $this->Imagick_getImageOrientation(); + + case 'signature': + return $this->Imagick_getImageSignature(); + + case 'software': + return $this->Imagick_getImageProperty('exif:Software'); + + case 'subsectime': + $this->subsectime = (int)$this->Imagick_getImageProperty('exif:SubSecTimeOriginal'); + + // In case of an error. + if ($this->subsectime > 32767) + $this->subsectime = 32767; + + if ($this->subsectime === FALSE) + $this->subsectime = 0; + + return $this->subsectime; + + case 'width': + return $this->Imagick_getImageWidth(); + + default: + throw new \Exception('To implement: '.$property); + } + } + + /** + * Return the image, rotated + */ + public function image(): ?string + { + $imo = clone($this->o); + + return $imo ? $this->rotate($imo) : NULL; } /** * Display the orientation of a photo */ - public function orientation() { + public function orientation(): string + { switch ($this->orientation) { case 1: return 'None!'; case 3: return 'Upside Down'; @@ -102,118 +234,36 @@ class Photo extends Abstracted\Catalog /** * Rotate the image */ - private function rotate(\Imagick $imo) + private function rotate(\Imagick $imo,string $format='jpg'): string { if (array_key_exists($this->orientation,$this->_rotate)) $imo->rotateImage(new \ImagickPixel('none'),$this->_rotate[$this->orientation]); - $imo->setImageFormat('jpg'); + $imo->setImageFormat($format); + + if (array_key_exists('exif',$imo->getImageProfiles())) + $imo->removeImageProfile('exif'); + return $imo->getImageBlob(); } - public function property(string $property) - { - if (! $this->o()) - return NULL; - - switch ($property) { - case 'creationdate': - if ($this->property('exif:DateTimeOriginal') == '0000:00:00 00:00:00' - && $this->property('exif:DateTimeOriginal') == '0000:00:00 00:00:00') - return NULL; - - return strtotime( - $this->property('exif:DateTimeOriginal') && $this->property('exif:DateTimeOriginal') != '0000:00:00 00:00:00' - ? $this->property('exif:DateTimeOriginal') - : $this->property('exif:DateTime')); - break; - - case 'height': return $this->_o->getImageHeight(); - case 'orientation': return $this->_o->getImageOrientation(); - case 'signature': return $this->_o->getImageSignature(); - case 'width': return $this->_o->getImageWidth(); - - default: - return $this->_o->getImageProperty($property); - } - } - - public function properties() - { - return $this->o() ? $this->_o->getImageProperties() : []; - } - - - public function setLocation() - { - $this->gps_lat = static::latlon(preg_split('/,\s?/',$this->property('exif:GPSLatitude')),$this->property('exif:GPSLatitudeRef')); - $this->gps_lon = static::latlon(preg_split('/,\s?/',$this->property('exif:GPSLongitude')),$this->property('exif:GPSLongitudeRef')); - } - - public function setMakeModel() - { - $ma = NULL; - - if ($this->property('exif:Make')) - $ma = Make::firstOrCreate([ - 'name'=>$this->property('exif:Make'), - ]); - - $mo = Model::firstOrCreate([ - 'name'=>$this->property('exif:Model') ?: NULL, - 'make_id'=>$ma ? $ma->id : NULL, - ]); - - $so = Software::firstOrCreate([ - 'name'=>$this->property('exif:Software') ?: NULL, - 'model_id'=>$mo->id, - ]); - - $this->software_id = $so->id; - } - - public function setSubSecTime() - { - $this->subsectime = (int)$this->property('exif:SubSecTimeOriginal'); - - // In case of an error. - if ($this->subsectime > 32767) - $this->subsectime = 32767; - - if ($this->subsectime === FALSE) - $this->subsectime = 0; - } - - public function setThumbnail() - { - try { - $this->thumbnail = exif_thumbnail($this->file_path()); - - } catch (\Exception $e) { - // @todo Couldnt get the thumbnail, so we should create one. - Log::info(sprintf('Unable to create thumbnail for %s (%s)',$this->id,$e->getMessage())); - } - - if ($this->thumbnail === FALSE) - $this->thumbnail = NULL; - } - /** * Return the image's thumbnail */ - public function thumbnail($rotate=TRUE) + public function thumbnail($rotate=TRUE): ?string { if (! $this->thumbnail) { - if ($this->isReadable() AND $this->o()->thumbnailimage(200,200,true,false)) { - $this->_o->setImageFormat('jpg'); - return $this->_o->getImageBlob(); + if ($this->isReadable() && $this->o->thumbnailimage(150,150,true)) { + $this->o->setImageFormat('jpg'); + + return $this->o->getImageBlob(); } else { return NULL; } } - if (! $rotate OR ! array_key_exists($this->orientation,$this->_rotate) OR ! extension_loaded('imagick')) + if ((! $rotate) || (! array_key_exists($this->orientation,$this->_rotate)) || (! extension_loaded('imagick'))) return $this->thumbnail; $imo = new \Imagick(); @@ -225,10 +275,9 @@ class Photo extends Abstracted\Catalog /** * Return the extension of the image - * @todo mime-by-ext? */ - public function type($mime=FALSE) + public function type(bool $mime=FALSE): string { - return strtolower($mime ? 'image/jpeg' : pathinfo($this->filename,PATHINFO_EXTENSION)); + return strtolower($mime ? mime_content_type($this->file_name(FALSE)) : pathinfo($this->filename,PATHINFO_EXTENSION)); } } \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3777dc8..fbb34d8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Illuminate\Foundation\Bus\DispatchesJobs; @@ -30,12 +31,14 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + Route::model('po',Photo::class); + // Any photo saved, queue it to be moved. Photo::saved(function($photo) { - if ($photo->scanned AND ! $photo->duplicate AND ! $photo->remove AND ($x=$photo->moveable()) === TRUE) { + if ($photo->scanned && (! $photo->duplicate) && (! $photo->remove) && ($x=$photo->moveable()) === TRUE) { Log::info(sprintf('%s: Need to Move [%s]',__METHOD__,$photo->id.'|'.serialize($x))); - $this->dispatch((new PhotoMove($photo))->onQueue('move')); + PhotoMove::dispatch($photo)->onQueue('move'); } }); diff --git a/app/Traits/Type.php b/app/Traits/Type.php index b0c72b4..9a34d02 100644 --- a/app/Traits/Type.php +++ b/app/Traits/Type.php @@ -12,7 +12,7 @@ trait Type { private function getModelType(string $type): string { - $class = 'App\Models\\'.$type; + $class = 'App\Models\\'.ucfirst(strtolower($type)); if (! class_exists($class)) abort(500,sprintf('No class [%s]',$type)); diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..42dec9a --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,5 @@ + env('FILESYSTEM_DISK', 'local'), + 'default' => env('FILESYSTEM_DISK', 'local'), - /* - |-------------------------------------------------------------------------- - | Filesystem Disks - |-------------------------------------------------------------------------- - | - | Below you may configure as many filesystem disks as necessary, and you - | may even configure multiple disks for the same driver. Examples for - | most supported storage drivers are configured here for reference. - | - | Supported drivers: "local", "ftp", "sftp", "s3" - | - */ + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ - 'disks' => [ + 'disks' => [ - 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - 'throw' => false, - ], + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + 'throw' => false, + ], - 'public' => [ - 'driver' => 'local', - 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', - 'visibility' => 'public', - 'throw' => false, - ], + 'nas' => [ + 'driver' => 'local', + 'root' => storage_path('nas'), + 'throw' => false, + ], - 's3' => [ - 'driver' => 's3', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION'), - 'bucket' => env('AWS_BUCKET'), - 'url' => env('AWS_URL'), - 'endpoint' => env('AWS_ENDPOINT'), - 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), - 'throw' => false, - ], + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + ], - ], + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + ], - /* - |-------------------------------------------------------------------------- - | Symbolic Links - |-------------------------------------------------------------------------- - | - | Here you may configure the symbolic links that will be created when the - | `storage:link` Artisan command is executed. The array keys should be - | the locations of the links and the values should be their targets. - | - */ + ], - 'links' => [ - public_path('storage') => storage_path('app/public'), - ], + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], ]; diff --git a/config/photo.php b/config/photo.php index 2852571..41779af 100644 --- a/config/photo.php +++ b/config/photo.php @@ -1,7 +1,7 @@ '/photos', + 'dir'=>'Photos', 'import'=>[ 'accepted'=>['jpg','jpeg','heic'], ], diff --git a/config/video.php b/config/video.php index dd78889..0cd3c9a 100644 --- a/config/video.php +++ b/config/video.php @@ -1,7 +1,7 @@ '/videos', + 'dir'=>'HomeMovies', 'import'=>[ 'accepted'=>['m4v','mov','mp4','avi'], ], diff --git a/public/js/maplibre-style.json b/public/js/maplibre-style.json new file mode 100644 index 0000000..aa8f3ae --- /dev/null +++ b/public/js/maplibre-style.json @@ -0,0 +1,2560 @@ +{ + "version": 8, + "name": "Bright", + "metadata": { + "mapbox:autocomposite": false, + "mapbox:groups": { + "1444849242106.713": {"collapsed": false, "name": "Places"}, + "1444849334699.1902": {"collapsed": true, "name": "Bridges"}, + "1444849345966.4436": {"collapsed": false, "name": "Roads"}, + "1444849354174.1904": {"collapsed": true, "name": "Tunnels"}, + "1444849364238.8171": {"collapsed": false, "name": "Buildings"}, + "1444849382550.77": {"collapsed": false, "name": "Water"}, + "1444849388993.3071": {"collapsed": false, "name": "Land"} + }, + "mapbox:type": "template", + "openmaptiles:mapbox:owner": "openmaptiles", + "openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t", + "openmaptiles:version": "3.x" + }, + "center": [0, 0], + "zoom": 1, + "bearing": 0, + "pitch": 0, + "sources": { + "openmaptiles": { + "type": "vector", + "url": "https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=hspQjLANaPwdHUrvUcsf" + } + }, + "sprite": "https://openmaptiles.github.io/osm-bright-gl-style/sprite", + "glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key=hspQjLANaPwdHUrvUcsf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": {"background-color": "#f8f4f0"} + }, + { + "id": "landcover-glacier", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", "subclass", "glacier"], + "layout": {"visibility": "visible"}, + "paint": { + "fill-color": "#fff", + "fill-opacity": {"base": 1, "stops": [[0, 0.9], [10, 0.3]]} + } + }, + { + "id": "landuse-residential", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["in", "class", "residential", "suburb", "neighbourhood"] + ], + "layout": {"visibility": "visible"}, + "paint": { + "fill-color": { + "base": 1, + "stops": [ + [12, "hsla(30, 19%, 90%, 0.4)"], + [16, "hsla(30, 19%, 90%, 0.2)"] + ] + } + } + }, + { + "id": "landuse-commercial", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["==", "$type", "Polygon"], + ["==", "class", "commercial"] + ], + "layout": {"visibility": "visible"}, + "paint": {"fill-color": "hsla(0, 60%, 87%, 0.23)"} + }, + { + "id": "landuse-industrial", + "type": "fill", + "source": "openmaptiles", + "source-layer": "landuse", + "filter": [ + "all", + ["==", "$type", "Polygon"], + ["in", "class", "industrial", "garages", "dam"] + ], + "layout": {"visibility": "visible"}, + "paint": {"fill-color": "hsla(49, 100%, 88%, 0.34)"} + }, + { + "id": "landuse-cemetery", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", "class", "cemetery"], + "paint": {"fill-color": "#e0e4dd"} + }, + { + "id": "landuse-hospital", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", "class", "hospital"], + "paint": {"fill-color": "#fde"} + }, + { + "id": "landuse-school", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", "class", "school"], + "paint": {"fill-color": "#f0e8f8"} + }, + { + "id": "landuse-railway", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landuse", + "filter": ["==", "class", "railway"], + "layout": {"visibility": "visible"}, + "paint": {"fill-color": "hsla(30, 19%, 90%, 0.4)"} + }, + { + "id": "landcover-wood", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", "class", "wood"], + "paint": { + "fill-antialias": {"base": 1, "stops": [[0, false], [9, true]]}, + "fill-color": "#6a4", + "fill-opacity": 0.1, + "fill-outline-color": "hsla(0, 0%, 0%, 0.03)" + } + }, + { + "id": "landcover-grass", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", "class", "grass"], + "paint": {"fill-color": "#d8e8c8", "fill-opacity": 1} + }, + { + "id": "landcover-grass-park", + "type": "fill", + "metadata": {"mapbox:group": "1444849388993.3071"}, + "source": "openmaptiles", + "source-layer": "park", + "filter": ["==", "class", "public_park"], + "paint": {"fill-color": "#d8e8c8", "fill-opacity": 0.8} + }, + { + "id": "waterway_tunnel", + "type": "line", + "source": "openmaptiles", + "source-layer": "waterway", + "minzoom": 14, + "filter": [ + "all", + ["in", "class", "river", "stream", "canal"], + ["==", "brunnel", "tunnel"] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [2, 4], + "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 6]]} + } + }, + { + "id": "waterway-other", + "type": "line", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["!in", "class", "canal", "river", "stream"], + ["==", "intermittent", 0] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 2]]} + } + }, + { + "id": "waterway-other-intermittent", + "type": "line", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["!in", "class", "canal", "river", "stream"], + ["==", "intermittent", 1] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [4, 3], + "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 2]]} + } + }, + { + "id": "waterway-stream-canal", + "type": "line", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["in", "class", "canal", "stream"], + ["!=", "brunnel", "tunnel"], + ["==", "intermittent", 0] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 6]]} + } + }, + { + "id": "waterway-stream-canal-intermittent", + "type": "line", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["in", "class", "canal", "stream"], + ["!=", "brunnel", "tunnel"], + ["==", "intermittent", 1] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [4, 3], + "line-width": {"base": 1.3, "stops": [[13, 0.5], [20, 6]]} + } + }, + { + "id": "waterway-river", + "type": "line", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["==", "class", "river"], + ["!=", "brunnel", "tunnel"], + ["==", "intermittent", 0] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-width": {"base": 1.2, "stops": [[10, 0.8], [20, 6]]} + } + }, + { + "id": "waterway-river-intermittent", + "type": "line", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "waterway", + "filter": [ + "all", + ["==", "class", "river"], + ["!=", "brunnel", "tunnel"], + ["==", "intermittent", 1] + ], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [3, 2.5], + "line-width": {"base": 1.2, "stops": [[10, 0.8], [20, 6]]} + } + }, + { + "id": "water-offset", + "type": "fill", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "water", + "maxzoom": 8, + "filter": ["==", "$type", "Polygon"], + "layout": {"visibility": "visible"}, + "paint": { + "fill-color": "#a0c8f0", + "fill-opacity": 1, + "fill-translate": {"base": 1, "stops": [[6, [2, 0]], [8, [0, 0]]]} + } + }, + { + "id": "water", + "type": "fill", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "water", + "filter": ["all", ["!=", "intermittent", 1], ["!=", "brunnel", "tunnel"]], + "layout": {"visibility": "visible"}, + "paint": {"fill-color": "hsl(210, 67%, 85%)"} + }, + { + "id": "water-intermittent", + "type": "fill", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "water", + "filter": ["all", ["==", "intermittent", 1]], + "layout": {"visibility": "visible"}, + "paint": {"fill-color": "hsl(210, 67%, 85%)", "fill-opacity": 0.7} + }, + { + "id": "water-pattern", + "type": "fill", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "water", + "filter": ["all"], + "layout": {"visibility": "visible"}, + "paint": {"fill-pattern": "wave", "fill-translate": [0, 2.5]} + }, + { + "id": "landcover-ice-shelf", + "type": "fill", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", "subclass", "ice_shelf"], + "layout": {"visibility": "visible"}, + "paint": { + "fill-color": "#fff", + "fill-opacity": {"base": 1, "stops": [[0, 0.9], [10, 0.3]]} + } + }, + { + "id": "landcover-sand", + "type": "fill", + "metadata": {"mapbox:group": "1444849382550.77"}, + "source": "openmaptiles", + "source-layer": "landcover", + "filter": ["==", "class", "sand"], + "layout": {"visibility": "visible"}, + "paint": {"fill-color": "rgba(245, 238, 188, 1)", "fill-opacity": 1} + }, + { + "id": "building", + "type": "fill", + "metadata": {"mapbox:group": "1444849364238.8171"}, + "source": "openmaptiles", + "source-layer": "building", + "paint": { + "fill-antialias": true, + "fill-color": {"base": 1, "stops": [[15.5, "#f2eae2"], [16, "#dfdbd7"]]} + } + }, + { + "id": "building-top", + "type": "fill", + "metadata": {"mapbox:group": "1444849364238.8171"}, + "source": "openmaptiles", + "source-layer": "building", + "layout": {"visibility": "visible"}, + "paint": { + "fill-color": "#f2eae2", + "fill-opacity": {"base": 1, "stops": [[13, 0], [16, 1]]}, + "fill-outline-color": "#dfdbd7", + "fill-translate": {"base": 1, "stops": [[14, [0, 0]], [16, [-2, -2]]]} + } + }, + { + "id": "tunnel-service-track-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "service", "track"] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-width": {"base": 1.2, "stops": [[15, 1], [16, 4], [20, 11]]} + } + }, + { + "id": "tunnel-motorway-link-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["==", "class", "motorway"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round", "visibility": "visible"}, + "paint": { + "line-color": "rgba(200, 147, 102, 1)", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] + } + } + }, + { + "id": "tunnel-minor-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "minor"]], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[12, 0.5], [13, 1], [14, 4], [20, 15]] + }, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "tunnel-link-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "trunk", "primary", "secondary", "tertiary"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] + }, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "tunnel-secondary-tertiary-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": {"base": 1.2, "stops": [[8, 1.5], [20, 17]]}, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "tunnel-trunk-primary-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "primary", "trunk"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 22]] + } + } + }, + { + "id": "tunnel-motorway-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["==", "class", "motorway"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round", "visibility": "visible"}, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 22]] + } + } + }, + { + "id": "tunnel-path-steps-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "tunnel"], + ["==", "class", "path"], + ["==", "subclass", "steps"] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[12, 0.5], [13, 1], [14, 2], [20, 9.25]] + }, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "tunnel-path-steps", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "tunnel"], + ["==", "class", "path"], + ["==", "subclass", "steps"] + ], + "layout": {"line-join": "bevel", "line-cap": "butt"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[13.5, 0], [14, 1.25], [20, 5.75]] + }, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "tunnel-path", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "tunnel"], + ["==", "class", "path"], + ["!=", "subclass", "steps"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 4]]} + } + }, + { + "id": "tunnel-motorway-link", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["==", "class", "motorway"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round", "visibility": "visible"}, + "paint": { + "line-color": "rgba(244, 209, 158, 1)", + "line-width": { + "base": 1.2, + "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] + } + } + }, + { + "id": "tunnel-service-track", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "service", "track"] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-width": {"base": 1.2, "stops": [[15.5, 0], [16, 2], [20, 7.5]]} + } + }, + { + "id": "tunnel-link", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "trunk", "primary", "secondary", "tertiary"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": { + "base": 1.2, + "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] + } + } + }, + { + "id": "tunnel-minor", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "minor"]], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": {"base": 1.2, "stops": [[13.5, 0], [14, 2.5], [20, 11.5]]} + } + }, + { + "id": "tunnel-secondary-tertiary", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 10]]} + } + }, + { + "id": "tunnel-trunk-primary", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["in", "class", "primary", "trunk"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fff4c6", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} + } + }, + { + "id": "tunnel-motorway", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "tunnel"], + ["==", "class", "motorway"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round", "visibility": "visible"}, + "paint": { + "line-color": "#ffdaa6", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} + } + }, + { + "id": "tunnel-railway", + "type": "line", + "metadata": {"mapbox:group": "1444849354174.1904"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [2, 2], + "line-width": {"base": 1.4, "stops": [[14, 0.4], [15, 0.75], [20, 2]]} + } + }, + { + "id": "ferry", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["in", "class", "ferry"]], + "layout": {"line-join": "round", "visibility": "visible"}, + "paint": { + "line-color": "rgba(108, 159, 182, 1)", + "line-dasharray": [2, 2], + "line-width": 1.1 + } + }, + { + "id": "aeroway-taxiway-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["all", ["in", "class", "taxiway"]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(153, 153, 153, 1)", + "line-opacity": 1, + "line-width": {"base": 1.5, "stops": [[11, 2], [17, 12]]} + } + }, + { + "id": "aeroway-runway-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["all", ["in", "class", "runway"]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(153, 153, 153, 1)", + "line-opacity": 1, + "line-width": {"base": 1.5, "stops": [[11, 5], [17, 55]]} + } + }, + { + "id": "aeroway-area", + "type": "fill", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["==", "$type", "Polygon"], + ["in", "class", "runway", "taxiway"] + ], + "layout": {"visibility": "visible"}, + "paint": { + "fill-color": "rgba(255, 255, 255, 1)", + "fill-opacity": {"base": 1, "stops": [[13, 0], [14, 1]]} + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["in", "class", "taxiway"], + ["==", "$type", "LineString"] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": {"base": 1, "stops": [[11, 0], [12, 1]]}, + "line-width": {"base": 1.5, "stops": [[11, 1], [17, 10]]} + } + }, + { + "id": "aeroway-runway", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "aeroway", + "minzoom": 4, + "filter": [ + "all", + ["in", "class", "runway"], + ["==", "$type", "LineString"] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(255, 255, 255, 1)", + "line-opacity": {"base": 1, "stops": [[11, 0], [12, 1]]}, + "line-width": {"base": 1.5, "stops": [[11, 4], [17, 50]]} + } + }, + { + "id": "road_area_pier", + "type": "fill", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "$type", "Polygon"], ["==", "class", "pier"]], + "layout": {"visibility": "visible"}, + "paint": {"fill-antialias": true, "fill-color": "#f8f4f0"} + }, + { + "id": "road_pier", + "type": "line", + "metadata": {}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "$type", "LineString"], ["in", "class", "pier"]], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#f8f4f0", + "line-width": {"base": 1.2, "stops": [[15, 1], [17, 4]]} + } + }, + { + "id": "highway-area", + "type": "fill", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "$type", "Polygon"], ["!in", "class", "pier"]], + "layout": {"visibility": "visible"}, + "paint": { + "fill-antialias": false, + "fill-color": "hsla(0, 0%, 89%, 0.56)", + "fill-opacity": 0.9, + "fill-outline-color": "#cfcdca" + } + }, + { + "id": "highway-path-steps-casing", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "path"], + ["in", "subclass", "steps"] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[12, 0.5], [13, 1], [14, 2], [20, 9.25]] + } + } + }, + { + "id": "highway-motorway-link-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 12, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "motorway"], + ["==", "ramp", 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] + } + } + }, + { + "id": "highway-link-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "trunk", "primary", "secondary", "tertiary"], + ["==", "ramp", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[12, 1], [13, 3], [14, 4], [20, 15]] + } + } + }, + { + "id": "highway-minor-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!=", "brunnel", "tunnel"], + ["in", "class", "minor", "service", "track"] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[12, 0.5], [13, 1], [14, 4], [20, 15]] + } + } + }, + { + "id": "highway-secondary-tertiary-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": {"base": 1.2, "stops": [[8, 1.5], [20, 17]]} + } + }, + { + "id": "highway-primary-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "primary"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#e9ac77", + "line-opacity": {"stops": [[7, 0], [8, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[7, 0], [8, 0.6], [9, 1.5], [20, 22]] + } + } + }, + { + "id": "highway-trunk-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "trunk"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#e9ac77", + "line-opacity": {"stops": [[5, 0], [6, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[5, 0], [6, 0.6], [7, 1.5], [20, 22]] + } + } + }, + { + "id": "highway-motorway-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 4, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "motorway"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#e9ac77", + "line-opacity": {"stops": [[4, 0], [5, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[4, 0], [5, 0.4], [6, 0.6], [7, 1.5], [20, 22]] + } + } + }, + { + "id": "highway-path", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "path"], + ["!=", "subclass", "steps"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 4]]} + } + }, + { + "id": "highway-path-steps", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "path"], + ["==", "subclass", "steps"] + ], + "layout": {"line-join": "bevel", "line-cap": "butt"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[13.5, 0], [14, 1.25], [20, 5.75]] + }, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "highway-motorway-link", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 12, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "motorway"], + ["==", "ramp", 1] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] + } + } + }, + { + "id": "highway-link", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "trunk", "primary", "secondary", "tertiary"], + ["==", "ramp", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] + } + } + }, + { + "id": "highway-minor", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!=", "brunnel", "tunnel"], + ["in", "class", "minor", "service", "track"] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": {"base": 1.2, "stops": [[13.5, 0], [14, 2.5], [20, 11.5]]} + } + }, + { + "id": "highway-secondary-tertiary", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fea", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [8, 0.5], [20, 13]]} + } + }, + { + "id": "highway-primary", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "primary"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fea", + "line-width": {"base": 1.2, "stops": [[8.5, 0], [9, 0.5], [20, 18]]} + } + }, + { + "id": "highway-trunk", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "trunk"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fea", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} + } + }, + { + "id": "highway-motorway", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 5, + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "motorway"], + ["!=", "ramp", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#fc8", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} + } + }, + { + "id": "railway-transit", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "class", "transit"], + ["!in", "brunnel", "tunnel"] + ], + "layout": {"visibility": "visible"}, + "paint": { + "line-color": "hsla(0, 0%, 73%, 0.77)", + "line-width": {"base": 1.4, "stops": [[14, 0.4], [20, 1]]} + } + }, + { + "id": "railway-transit-hatching", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "class", "transit"], + ["!in", "brunnel", "tunnel"] + ], + "layout": {"visibility": "visible"}, + "paint": { + "line-color": "hsla(0, 0%, 73%, 0.68)", + "line-dasharray": [0.2, 8], + "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 2], [20, 6]]} + } + }, + { + "id": "railway-service", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "class", "rail"], + ["has", "service"] + ], + "paint": { + "line-color": "hsla(0, 0%, 73%, 0.77)", + "line-width": {"base": 1.4, "stops": [[14, 0.4], [20, 1]]} + } + }, + { + "id": "railway-service-hatching", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "class", "rail"], + ["has", "service"] + ], + "layout": {"visibility": "visible"}, + "paint": { + "line-color": "hsla(0, 0%, 73%, 0.68)", + "line-dasharray": [0.2, 8], + "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 2], [20, 6]]} + } + }, + { + "id": "railway", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!has", "service"], + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-width": {"base": 1.4, "stops": [[14, 0.4], [15, 0.75], [20, 2]]} + } + }, + { + "id": "railway-hatching", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!has", "service"], + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "class", "rail"] + ], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 3], [20, 8]]} + } + }, + { + "id": "bridge-motorway-link-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["==", "class", "motorway"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[12, 1], [13, 3], [14, 4], [20, 19]] + } + } + }, + { + "id": "bridge-link-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["in", "class", "trunk", "primary", "secondary", "tertiary"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[12, 1], [13, 3], [14, 4], [20, 19]] + } + } + }, + { + "id": "bridge-secondary-tertiary-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[5, 0.4], [7, 0.6], [8, 1.5], [20, 21]] + } + } + }, + { + "id": "bridge-trunk-primary-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["in", "class", "primary", "trunk"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "hsl(28, 76%, 67%)", + "line-width": { + "base": 1.2, + "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 26]] + } + } + }, + { + "id": "bridge-motorway-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["==", "class", "motorway"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [[5, 0.4], [6, 0.6], [7, 1.5], [20, 26]] + } + } + }, + { + "id": "bridge-minor-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["in", "class", "minor", "service", "track"] + ], + "layout": {"line-cap": "butt", "line-join": "round"}, + "paint": { + "line-color": "#cfcdca", + "line-opacity": {"stops": [[12, 0], [12.5, 1]]}, + "line-width": { + "base": 1.2, + "stops": [[12, 0.5], [13, 1], [14, 6], [20, 24]] + } + } + }, + { + "id": "bridge-path-casing", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["==", "class", "path"] + ], + "paint": { + "line-color": "#cfcdca", + "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 18]]} + } + }, + { + "id": "bridge-path-steps", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["==", "class", "path"], + ["==", "subclass", "steps"] + ], + "layout": {"line-join": "round", "line-cap": "butt"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": { + "base": 1.2, + "stops": [[13.5, 0], [14, 1.25], [20, 5.75]] + }, + "line-dasharray": [0.5, 0.25] + } + }, + { + "id": "bridge-path", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["==", "class", "path"], + ["!=", "subclass", "steps"] + ], + "paint": { + "line-color": "#cba", + "line-dasharray": [1.5, 0.75], + "line-width": {"base": 1.2, "stops": [[15, 1.2], [20, 4]]} + } + }, + { + "id": "bridge-motorway-link", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["==", "class", "motorway"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] + } + } + }, + { + "id": "bridge-link", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["in", "class", "trunk", "primary", "secondary", "tertiary"], + ["==", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [[12.5, 0], [13, 1.5], [14, 2.5], [20, 11.5]] + } + } + }, + { + "id": "bridge-minor", + "type": "line", + "metadata": {"mapbox:group": "1444849345966.4436"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["in", "class", "minor", "service", "track"] + ], + "layout": {"line-cap": "round", "line-join": "round"}, + "paint": { + "line-color": "#fff", + "line-opacity": 1, + "line-width": {"base": 1.2, "stops": [[13.5, 0], [14, 2.5], [20, 11.5]]} + } + }, + { + "id": "bridge-secondary-tertiary", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [8, 0.5], [20, 13]]} + } + }, + { + "id": "bridge-trunk-primary", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["in", "class", "primary", "trunk"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fea", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} + } + }, + { + "id": "bridge-motorway", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "brunnel", "bridge"], + ["==", "class", "motorway"], + ["!=", "ramp", 1] + ], + "layout": {"line-join": "round"}, + "paint": { + "line-color": "#fc8", + "line-width": {"base": 1.2, "stops": [[6.5, 0], [7, 0.5], [20, 18]]} + } + }, + { + "id": "bridge-railway", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-width": {"base": 1.4, "stops": [[14, 0.4], [15, 0.75], [20, 2]]} + } + }, + { + "id": "bridge-railway-hatching", + "type": "line", + "metadata": {"mapbox:group": "1444849334699.1902"}, + "source": "openmaptiles", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": {"base": 1.4, "stops": [[14.5, 0], [15, 3], [20, 8]]} + } + }, + { + "id": "cablecar", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["==", "subclass", "cable_car"], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "hsl(0, 0%, 70%)", + "line-width": {"base": 1, "stops": [[11, 1], [19, 2.5]]} + } + }, + { + "id": "cablecar-dash", + "type": "line", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["==", "subclass", "cable_car"], + "layout": {"line-cap": "round", "visibility": "visible"}, + "paint": { + "line-color": "hsl(0, 0%, 70%)", + "line-dasharray": [2, 3], + "line-width": {"base": 1, "stops": [[11, 3], [19, 5.5]]} + } + }, + { + "id": "boundary-land-level-4", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 2, + "filter": [ + "all", + [">=", "admin_level", 3], + ["<=", "admin_level", 8], + ["!=", "maritime", 1] + ], + "layout": {"line-join": "round", "visibility": "visible"}, + "paint": { + "line-color": "#9e9cab", + "line-dasharray": [3, 1, 1, 1], + "line-width": {"base": 1.4, "stops": [[4, 0.4], [5, 1], [12, 3]]} + } + }, + { + "id": "boundary-land-level-2", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "filter": [ + "all", + ["==", "admin_level", 2], + ["!=", "maritime", 1], + ["!=", "disputed", 1] + ], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(248, 7%, 66%)", + "line-width": { + "base": 1, + "stops": [[0, 0.6], [4, 1.4], [5, 2], [12, 8]] + } + } + }, + { + "id": "boundary-land-disputed", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "filter": ["all", ["!=", "maritime", 1], ["==", "disputed", 1]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(248, 7%, 70%)", + "line-dasharray": [1, 3], + "line-width": { + "base": 1, + "stops": [[0, 0.6], [4, 1.4], [5, 2], [12, 8]] + } + } + }, + { + "id": "boundary-water", + "type": "line", + "source": "openmaptiles", + "source-layer": "boundary", + "minzoom": 4, + "filter": ["all", ["in", "admin_level", 2, 4], ["==", "maritime", 1]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(154, 189, 214, 1)", + "line-opacity": {"stops": [[6, 0.6], [10, 1]]}, + "line-width": { + "base": 1, + "stops": [[0, 0.6], [4, 1.4], [5, 2], [12, 8]] + } + } + }, + { + "id": "waterway-name", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "waterway", + "minzoom": 13, + "filter": ["all", ["==", "$type", "LineString"], ["has", "name"]], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{name:latin} {name:nonlatin}", + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-rotation-alignment": "map", + "text-size": 14 + }, + "paint": { + "text-color": "#74aee9", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water-name-lakeline", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": ["==", "$type", "LineString"], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 350, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-rotation-alignment": "map", + "text-size": 14 + }, + "paint": { + "text-color": "#74aee9", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water-name-ocean", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": ["all", ["==", "$type", "Point"], ["==", "class", "ocean"]], + "layout": { + "symbol-placement": "point", + "symbol-spacing": 350, + "text-field": "{name:latin}", + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-rotation-alignment": "map", + "text-size": 14 + }, + "paint": { + "text-color": "#74aee9", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "water-name-other", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "water_name", + "filter": ["all", ["==", "$type", "Point"], ["!in", "class", "ocean"]], + "layout": { + "symbol-placement": "point", + "symbol-spacing": 350, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Italic"], + "text-letter-spacing": 0.2, + "text-max-width": 5, + "text-rotation-alignment": "map", + "text-size": {"stops": [[0, 10], [6, 14]]}, + "visibility": "visible" + }, + "paint": { + "text-color": "#74aee9", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1.5 + } + }, + { + "id": "road_oneway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + ["==", "oneway", 1], + [ + "in", + "class", + "motorway", + "trunk", + "primary", + "secondary", + "tertiary", + "minor", + "service" + ] + ], + "layout": { + "icon-image": "oneway", + "icon-padding": 2, + "icon-rotate": 90, + "icon-rotation-alignment": "map", + "icon-size": {"stops": [[15, 0.5], [19, 1]]}, + "symbol-placement": "line", + "symbol-spacing": 75 + }, + "paint": {"icon-opacity": 0.5} + }, + { + "id": "road_oneway_opposite", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation", + "minzoom": 15, + "filter": [ + "all", + ["==", "oneway", -1], + [ + "in", + "class", + "motorway", + "trunk", + "primary", + "secondary", + "tertiary", + "minor", + "service" + ] + ], + "layout": { + "icon-image": "oneway", + "icon-padding": 2, + "icon-rotate": -90, + "icon-rotation-alignment": "map", + "icon-size": {"stops": [[15, 0.5], [19, 1]]}, + "symbol-placement": "line", + "symbol-spacing": 75 + }, + "paint": {"icon-opacity": 0.5} + }, + { + "id": "poi-level-3", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 16, + "filter": [ + "all", + ["==", "$type", "Point"], + [">=", "rank", 25], + ["any", ["!has", "level"], ["==", "level", 0]] + ], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-padding": 2, + "text-size": 12, + "visibility": "visible" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi-level-2", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 15, + "filter": [ + "all", + ["==", "$type", "Point"], + ["<=", "rank", 24], + [">=", "rank", 15], + ["any", ["!has", "level"], ["==", "level", 0]] + ], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-padding": 2, + "text-size": 12, + "visibility": "visible" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi-level-1", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 14, + "filter": [ + "all", + ["==", "$type", "Point"], + ["<=", "rank", 14], + ["has", "name"], + ["any", ["!has", "level"], ["==", "level", 0]] + ], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-padding": 2, + "text-size": 12, + "visibility": "visible" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi-railway", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "poi", + "minzoom": 13, + "filter": [ + "all", + ["==", "$type", "Point"], + ["has", "name"], + ["==", "class", "railway"], + ["==", "subclass", "station"] + ], + "layout": { + "icon-allow-overlap": false, + "icon-ignore-placement": false, + "icon-image": "{class}_11", + "icon-optional": false, + "text-allow-overlap": false, + "text-anchor": "top", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-ignore-placement": false, + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-optional": true, + "text-padding": 2, + "text-size": 12 + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "highway-name-path", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 15.5, + "filter": ["==", "class", "path"], + "layout": { + "symbol-placement": "line", + "text-field": "{name:latin} {name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": {"base": 1, "stops": [[13, 12], [14, 13]]} + }, + "paint": { + "text-color": "hsl(30, 23%, 62%)", + "text-halo-color": "#f8f4f0", + "text-halo-width": 0.5 + } + }, + { + "id": "highway-name-minor", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 15, + "filter": [ + "all", + ["==", "$type", "LineString"], + ["in", "class", "minor", "service", "track"] + ], + "layout": { + "symbol-placement": "line", + "text-field": "{name:latin} {name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": {"base": 1, "stops": [[13, 12], [14, 13]]} + }, + "paint": { + "text-color": "#765", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "highway-name-major", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 12.2, + "filter": ["in", "class", "primary", "secondary", "tertiary", "trunk"], + "layout": { + "symbol-placement": "line", + "text-field": "{name:latin} {name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "map", + "text-size": {"base": 1, "stops": [[13, 12], [14, 13]]} + }, + "paint": { + "text-color": "#765", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "highway-shield", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 8, + "filter": [ + "all", + ["<=", "ref_length", 6], + ["==", "$type", "LineString"], + ["!in", "network", "us-interstate", "us-highway", "us-state"] + ], + "layout": { + "icon-image": "road_{ref_length}", + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": {"base": 1, "stops": [[10, "point"], [11, "line"]]}, + "symbol-spacing": 200, + "text-field": "{ref}", + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + }, + "paint": {} + }, + { + "id": "highway-shield-us-interstate", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 7, + "filter": [ + "all", + ["<=", "ref_length", 6], + ["==", "$type", "LineString"], + ["in", "network", "us-interstate"] + ], + "layout": { + "icon-image": "{network}_{ref_length}", + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": { + "base": 1, + "stops": [[7, "point"], [7, "line"], [8, "line"]] + }, + "symbol-spacing": 200, + "text-field": "{ref}", + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + }, + "paint": {"text-color": "rgba(0, 0, 0, 1)"} + }, + { + "id": "highway-shield-us-other", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "transportation_name", + "minzoom": 9, + "filter": [ + "all", + ["<=", "ref_length", 6], + ["==", "$type", "LineString"], + ["in", "network", "us-highway", "us-state"] + ], + "layout": { + "icon-image": "{network}_{ref_length}", + "icon-rotation-alignment": "viewport", + "icon-size": 1, + "symbol-placement": {"base": 1, "stops": [[10, "point"], [11, "line"]]}, + "symbol-spacing": 200, + "text-field": "{ref}", + "text-font": ["Noto Sans Regular"], + "text-rotation-alignment": "viewport", + "text-size": 10 + }, + "paint": {"text-color": "rgba(0, 0, 0, 1)"} + }, + { + "id": "airport-label-major", + "type": "symbol", + "source": "openmaptiles", + "source-layer": "aerodrome_label", + "minzoom": 10, + "filter": ["all", ["has", "iata"]], + "layout": { + "icon-image": "airport_11", + "icon-size": 1, + "text-anchor": "top", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-optional": true, + "text-padding": 2, + "text-size": 12, + "visibility": "visible" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "place-other", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": [ + "!in", + "class", + "city", + "town", + "village", + "state", + "country", + "continent" + ], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Bold"], + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-size": {"base": 1.2, "stops": [[12, 10], [15, 14]]}, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#633", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place-village", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": ["==", "class", "village"], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": {"base": 1.2, "stops": [[10, 12], [15, 22]]}, + "visibility": "visible" + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place-town", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": ["==", "class", "town"], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": {"base": 1.2, "stops": [[10, 14], [15, 24]]}, + "visibility": "visible" + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place-city", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": ["all", ["!=", "capital", 2], ["==", "class", "city"]], + "layout": { + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-size": {"base": 1.2, "stops": [[7, 14], [11, 24]]}, + "visibility": "visible" + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place-city-capital", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": ["all", ["==", "capital", 2], ["==", "class", "city"]], + "layout": { + "icon-image": "star_11", + "icon-size": 0.8, + "text-anchor": "left", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": ["Noto Sans Regular"], + "text-max-width": 8, + "text-offset": [0.4, 0], + "text-size": {"base": 1.2, "stops": [[7, 14], [11, 24]]}, + "visibility": "visible" + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place-state", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": ["in", "class", "state"], + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Bold"], + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-size": {"base": 1.2, "stops": [[12, 10], [15, 14]]}, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#633", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place-country-other", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": [ + "all", + ["==", "class", "country"], + [">=", "rank", 3], + ["!has", "iso_a2"] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Italic"], + "text-max-width": 6.25, + "text-size": {"stops": [[3, 11], [7, 17]]}, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2 + } + }, + { + "id": "place-country-3", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": [ + "all", + ["==", "class", "country"], + [">=", "rank", 3], + ["has", "iso_a2"] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": {"stops": [[3, 11], [7, 17]]}, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2 + } + }, + { + "id": "place-country-2", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": [ + "all", + ["==", "class", "country"], + ["==", "rank", 2], + ["has", "iso_a2"] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": {"stops": [[2, 11], [5, 17]]}, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2 + } + }, + { + "id": "place-country-1", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "filter": [ + "all", + ["==", "class", "country"], + ["==", "rank", 1], + ["has", "iso_a2"] + ], + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": {"stops": [[1, 11], [4, 17]]}, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2 + } + }, + { + "id": "place-continent", + "type": "symbol", + "metadata": {"mapbox:group": "1444849242106.713"}, + "source": "openmaptiles", + "source-layer": "place", + "maxzoom": 1, + "filter": ["==", "class", "continent"], + "layout": { + "text-field": "{name:latin}", + "text-font": ["Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": 14, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 2 + } + } + ], + "id": "bright" +} diff --git a/resources/views/catalog/deletereview.blade.php b/resources/views/catalog/deletereview.blade.php index 45c628c..7825e89 100644 --- a/resources/views/catalog/deletereview.blade.php +++ b/resources/views/catalog/deletereview.blade.php @@ -20,14 +20,13 @@ {{ $catalog->links() }}
- {{ csrf_field() }} + @csrf + @include('catalog.widgets.duplicates')
- -
@else NONE! diff --git a/resources/views/catalog/duplicatereview.blade.php b/resources/views/catalog/duplicatereview.blade.php index a84d02a..dccc320 100644 --- a/resources/views/catalog/duplicatereview.blade.php +++ b/resources/views/catalog/duplicatereview.blade.php @@ -20,14 +20,14 @@ {{ $catalog->links() }}
- {{ csrf_field() }} + @csrf + + @include('catalog.widgets.duplicates')
- -
@else NONE! diff --git a/resources/views/catalog/widgets/duplicates.blade.php b/resources/views/catalog/widgets/duplicates.blade.php index d5f069e..c78a4ad 100644 --- a/resources/views/catalog/widgets/duplicates.blade.php +++ b/resources/views/catalog/widgets/duplicates.blade.php @@ -1,4 +1,4 @@ - +
@@ -6,20 +6,18 @@ - @php - // Remember what we have rendered - $rendered = collect(); - @endphp + + @php($rendered = collect()) + @foreach ($catalog as $o) - @if($rendered->search($o->id)) @continue @endif - @php($rendered->push($o->id)) + @continue($rendered->search($o)) + @php($rendered->push($o)) - @if (! ($d=$o->myduplicates()->get())->count()) @@ -30,15 +28,15 @@ @else @foreach($d as $item) - @if($rendered->search($item->id)) @continue @endif - @php($rendered->push($item->id)) + @continue($rendered->search($item)) + @php($rendered->push($item)) @endforeach @endif - @endforeach +
Remove
- @include($o->type.'.widgets.thumbnail',['o'=>$o]) + @include($o::config.'.widgets.thumbnail',['o'=>$o,'reference'=>$o->newInstance()]) - @include($item->type.'.widgets.thumbnail',['o'=>$item]) + @include($item::config.'.widgets.thumbnail',['o'=>$item,'reference'=>$o])
\ No newline at end of file diff --git a/resources/views/components/info.blade.php b/resources/views/components/info.blade.php new file mode 100644 index 0000000..3464632 --- /dev/null +++ b/resources/views/components/info.blade.php @@ -0,0 +1 @@ +{{ $id }} \ No newline at end of file diff --git a/resources/views/components/thumbnail.blade.php b/resources/views/components/thumbnail.blade.php new file mode 100644 index 0000000..13022e2 --- /dev/null +++ b/resources/views/components/thumbnail.blade.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/views/photo/view.blade.php b/resources/views/photo/view.blade.php index 23e0c8e..62eb8d5 100644 --- a/resources/views/photo/view.blade.php +++ b/resources/views/photo/view.blade.php @@ -1,117 +1,137 @@ @extends('adminlte::layouts.app') @section('htmlheader_title') - Photo - {{ $o->id }} + Photo - {{ $po->id }} @endsection @section('contentheader_title') - Photo #{{ $o->id }} + Photo #{{ $po->id }} @endsection @section('contentheader_description') - @if(! $o->scanned)@endif - @if($o->duplicate)@endif - @if($o->ignore_duplicate)@endif - @if($o->remove)@endif + @endsection @section('page_title') - #{{ $o->id }} + #{{ $po->id }} @endsection @section('main-content')
- {!! $o->getHtmlImageURL() !!} + - - - +
-
-
Signature
{{ $o->signature(TRUE) }}
-
Filename
{{ $o->filename }}
+
+
+
+ @if(! $po->scanned)@endif + @if($po->duplicate)@endif + @if($po->ignore_duplicate)@endif + @if($po->remove)@endif +
+
+
Signature
{{ $po->signature(TRUE) }}
+
Filename
{{ $po->filename }}
- @if ($o->shouldMove()) -
NEW Filename
{{ $o->file_name() }}
- @endif + @if($po->shouldMove()) +
NEW Filename
{{ $po->file_name() }}
+ @endif -
Size
{{ $o->file_size() }}
-
Dimensions
{{ $o->dimensions }} @ {{ $o->orientation }}
-
-
Date Taken
{{ $o->date_taken() }}
-
Camera
{{ $o->device() }}
-
-
Location
-
- @if($o->gps() == 'UNKNOWN') - UNKNOWN - @else -
- @endif -
-
-
Exif Data
- - @foreach ($o->properties() as $k => $v) - - @endforeach -
{{ $k }}<>{{ $v }}
-
-
- @if(($x=$o->myduplicates()->get())->count()) -
Duplicates
-
- @foreach($x as $oo) - @if(! $loop->first)| @endif - {!! $oo->id_link !!} - @endforeach -
- @endif +
Size
{{ number_format($po->file_size,0) }}
+
Dimensions
{{ $po->dimensions }} @ {{ $po->orientation }}
+
+
Date Taken
{{ $po->created?->format('Y-m-d H:i:s') }}
+ + @if($po->scanned) +
Camera
{{ $po->device}}
+
+
Location
+
+ @if(! $po->gps) + UNKNOWN + @else +
+ @endif +
+ @endif + + @if($x=$po->Imagick_getImageProperties()) +
+ +
Exif Data
+ + @foreach($x as $k => $v) + + + @endforeach +
{{ $k }}{{ $v }} +
+
+ @endif + + @if(($x=$po->myduplicates()->get())->count()) +
+
Duplicates
+
+ @foreach($x as $oo) + @if(! $loop->first)| @endif + + @endforeach +
+ @endif +
+
- @if ($o->remove) -
+ @if($po->remove) + @else - + @endif - {{ csrf_field() }} + @csrf
@endsection @section('page-scripts') - @if($o->gps() !== 'UNKNOWN') - @js('//maps.google.com/maps/api/js?sensor=false') + @if($po->gps) + + @endif @append \ No newline at end of file diff --git a/resources/views/photo/widgets/thumbnail.blade.php b/resources/views/photo/widgets/thumbnail.blade.php index 4b5407f..8d3bc74 100644 --- a/resources/views/photo/widgets/thumbnail.blade.php +++ b/resources/views/photo/widgets/thumbnail.blade.php @@ -1,5 +1,4 @@ ['id','idlink'], 'Signature'=>['signature','signature'], 'File Signature'=>['file_signature','file_signature'], 'Date Created'=>['created','created'], @@ -15,27 +14,29 @@
- #{{ $o->id }} - {{ \Illuminate\Support\Str::limit($o->filename,12).\Illuminate\Support\Str::substr($o->filename,-16) }} - {{ $o->created ? $o->created->toDateTimeString() : '-' }} [{{ $o->device() }}] + #{{ $o->id }} - {{ Str::limit($o->filename,12).Str::substr($o->filename,-16) }} + {{ $o->created ? $o->created->toDateTimeString() : '-' }} @if($o->device)[{{ $o->device }}]@else [No Device Info] @endif
- +
-
- - diff --git a/routes/web.php b/routes/web.php index 56b7677..41129e2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,8 +15,8 @@ Route::get('/p/duplicates/{id?}',[PhotoController::class,'duplicates']) Route::get('/v/duplicates/{id?}',[VideoController::class,'duplicates']) ->where('id','[0-9]+'); -Route::get('/p/info/{o}',[PhotoController::class,'info']) - ->where('o','[0-9]+'); +Route::view('/p/info/{po}','photo.view') + ->where('po','[0-9]+'); Route::get('/v/info/{o}',[VideoController::class,'info']) ->where('o','[0-9]+'); Route::get('/p/thumbnail/{o}',[PhotoController::class,'thumbnail'])