Skip to content

Commit 993044c

Browse files
committed
Implement VideoCsvParser
1 parent f1c8122 commit 993044c

File tree

11 files changed

+545
-83
lines changed

11 files changed

+545
-83
lines changed

app/Rules/VideoMetadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class VideoMetadata extends ImageMetadata
77
/**
88
* {@inheritdoc}
99
*/
10-
public function passes($attribute, $value)
10+
public function passes($attribute, $value): bool
1111
{
1212
$passes = parent::passes($attribute, $value);
1313

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace Biigle\Services\MetadataParsing;
4+
5+
use duncan3dc\Bom\Util;
6+
use SeekableIterator;
7+
use SplFileObject;
8+
9+
abstract class CsvParser extends MetadataParser
10+
{
11+
/**
12+
* Column name synonyms.
13+
*
14+
* @var array
15+
*/
16+
const COLUMN_SYNONYMS = [
17+
'file' => 'filename',
18+
'lon' => 'lng',
19+
'longitude' => 'lng',
20+
'latitude' => 'lat',
21+
'heading' => 'yaw',
22+
'sub_datetime' => 'taken_at',
23+
'sub_longitude' => 'lng',
24+
'sub_latitude' => 'lat',
25+
'sub_heading' => 'yaw',
26+
'sub_distance' => 'distance_to_ground',
27+
'sub_altitude' => 'gps_altitude',
28+
];
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function recognizesFile(): bool
34+
{
35+
$file = $this->getCsvIterator();
36+
$line = $file->current();
37+
if (!is_array($line)) {
38+
return false;
39+
}
40+
41+
$line = $this->processFirstLine($line);
42+
43+
if (!in_array('filename', $line, true) && !in_array('file', $line, true)) {
44+
return false;
45+
}
46+
47+
return true;
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
abstract public function getMetadata(): VolumeMetadata;
54+
55+
protected function getCsvIterator(): SeekableIterator
56+
{
57+
$file = parent::getFileObject();
58+
$file->setFlags(SplFileObject::READ_CSV);
59+
60+
return $file;
61+
}
62+
63+
protected function processFirstLine(array $line): array
64+
{
65+
$line = array_map('strtolower', $line);
66+
if (!empty($line[0])) {
67+
$line[0] = Util::removeBom($line[0]);
68+
}
69+
70+
return $line;
71+
}
72+
73+
protected function getKeyMap(array $line): array
74+
{
75+
$line = $this->processFirstLine($line);
76+
77+
$keys = array_map(function ($column) {
78+
if (array_key_exists($column, self::COLUMN_SYNONYMS)) {
79+
return self::COLUMN_SYNONYMS[$column];
80+
}
81+
82+
return $column;
83+
}, $line);
84+
85+
// This will remove duplicate columns and retain the "last" one.
86+
return array_flip($keys);
87+
}
88+
89+
/**
90+
* Cast the value to float if it is not null or an empty string.
91+
*/
92+
protected function maybeCastToFloat(?string $value): ?float
93+
{
94+
return (is_null($value) || $value === '') ? null : floatval($value);
95+
}
96+
}

app/Services/MetadataParsing/ImageCsvParser.php

Lines changed: 10 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,9 @@
33
namespace Biigle\Services\MetadataParsing;
44

55
use Biigle\MediaType;
6-
use duncan3dc\Bom\Util;
7-
use SeekableIterator;
8-
use SplFileObject;
9-
use Symfony\Component\HttpFoundation\File\File;
106

11-
class ImageCsvParser extends MetadataParser
7+
class ImageCsvParser extends CsvParser
128
{
13-
/**
14-
* Column name synonyms.
15-
*
16-
* @var array
17-
*/
18-
const COLUMN_SYNONYMS = [
19-
'file' => 'filename',
20-
'lon' => 'lng',
21-
'longitude' => 'lng',
22-
'latitude' => 'lat',
23-
'heading' => 'yaw',
24-
'sub_datetime' => 'taken_at',
25-
'sub_longitude' => 'lng',
26-
'sub_latitude' => 'lat',
27-
'sub_heading' => 'yaw',
28-
'sub_distance' => 'distance_to_ground',
29-
'sub_altitude' => 'gps_altitude',
30-
];
31-
32-
/**
33-
* {@inheritdoc}
34-
*/
35-
public function recognizesFile(): bool
36-
{
37-
$file = $this->getCsvIterator();
38-
$line = $file->current();
39-
if (!is_array($line)) {
40-
return false;
41-
}
42-
43-
$line = $this->processFirstLine($line);
44-
45-
if (!in_array('filename', $line, true) && !in_array('file', $line, true)) {
46-
return false;
47-
}
48-
49-
return true;
50-
}
51-
529
/**
5310
* {@inheritdoc}
5411
*/
@@ -57,25 +14,14 @@ public function getMetadata(): VolumeMetadata
5714
$data = new VolumeMetadata(MediaType::image());
5815

5916
$file = $this->getCsvIterator();
60-
$keys = $file->current();
61-
if (!is_array($keys)) {
17+
$line = $file->current();
18+
if (!is_array($line)) {
6219
return $data;
6320
}
6421

65-
$keys = $this->processFirstLine($keys);
66-
$keys = array_map(function ($column) {
67-
if (array_key_exists($column, self::COLUMN_SYNONYMS)) {
68-
return self::COLUMN_SYNONYMS[$column];
69-
}
70-
71-
return $column;
72-
}, $keys);
73-
74-
// This will remove duplicate columns and retain the "last" one.
75-
$keyMap = array_flip($keys);
22+
$keyMap = $this->getKeyMap($line);
7623

7724
$getValue = fn ($row, $key) => $row[$keyMap[$key] ?? null] ?? null;
78-
$maybeCast = fn ($value) => (is_null($value) || $value === '') ? null : floatval($value);
7925

8026
$file->next();
8127
while ($file->valid()) {
@@ -92,36 +38,18 @@ public function getMetadata(): VolumeMetadata
9238

9339
$fileData = new ImageMetadata(
9440
name: $getValue($row, 'filename'),
95-
lat: $maybeCast($getValue($row, 'lat')),
96-
lng: $maybeCast($getValue($row, 'lng')),
41+
lat: $this->maybeCastToFloat($getValue($row, 'lat')),
42+
lng: $this->maybeCastToFloat($getValue($row, 'lng')),
9743
takenAt: $getValue($row, 'taken_at') ?: null, // Use null instead of ''.
98-
area: $maybeCast($getValue($row, 'area')),
99-
distanceToGround: $maybeCast($getValue($row, 'distance_to_ground')),
100-
gpsAltitude: $maybeCast($getValue($row, 'gps_altitude')),
101-
yaw: $maybeCast($getValue($row, 'yaw')),
44+
area: $this->maybeCastToFloat($getValue($row, 'area')),
45+
distanceToGround: $this->maybeCastToFloat($getValue($row, 'distance_to_ground')),
46+
gpsAltitude: $this->maybeCastToFloat($getValue($row, 'gps_altitude')),
47+
yaw: $this->maybeCastToFloat($getValue($row, 'yaw')),
10248
);
10349

10450
$data->addFile($fileData);
10551
}
10652

10753
return $data;
10854
}
109-
110-
protected function getCsvIterator(): SeekableIterator
111-
{
112-
$file = parent::getFileObject();
113-
$file->setFlags(SplFileObject::READ_CSV);
114-
115-
return $file;
116-
}
117-
118-
protected function processFirstLine(array $line): array
119-
{
120-
$line = array_map('strtolower', $line);
121-
if (!empty($line[0])) {
122-
$line[0] = Util::removeBom($line[0]);
123-
}
124-
125-
return $line;
126-
}
12755
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Biigle\Services\MetadataParsing;
4+
5+
use Biigle\MediaType;
6+
7+
class VideoCsvParser extends CsvParser
8+
{
9+
/**
10+
* {@inheritdoc}
11+
*/
12+
public function getMetadata(): VolumeMetadata
13+
{
14+
$data = new VolumeMetadata(MediaType::video());
15+
16+
$file = $this->getCsvIterator();
17+
$line = $file->current();
18+
if (!is_array($line)) {
19+
return $data;
20+
}
21+
22+
$keyMap = $this->getKeyMap($line);
23+
24+
$getValue = fn ($row, $key) => $row[$keyMap[$key] ?? null] ?? null;
25+
26+
$file->next();
27+
while ($file->valid()) {
28+
$row = $file->current();
29+
$file->next();
30+
if (empty($row)) {
31+
continue;
32+
}
33+
34+
$name = $getValue($row, 'filename');
35+
if (empty($name)) {
36+
continue;
37+
}
38+
39+
// Use null instead of ''.
40+
$takenAt = $getValue($row, 'taken_at') ?: null;
41+
42+
// If the file already exists but takenAt is null, replace the file by newly
43+
// adding it.
44+
if (!is_null($fileData = $data->getFile($name)) && !is_null($takenAt)) {
45+
$fileData->addFrame(
46+
takenAt: $takenAt,
47+
lat: $this->maybeCastToFloat($getValue($row, 'lat')),
48+
lng: $this->maybeCastToFloat($getValue($row, 'lng')),
49+
area: $this->maybeCastToFloat($getValue($row, 'area')),
50+
distanceToGround: $this->maybeCastToFloat($getValue($row, 'distance_to_ground')),
51+
gpsAltitude: $this->maybeCastToFloat($getValue($row, 'gps_altitude')),
52+
yaw: $this->maybeCastToFloat($getValue($row, 'yaw')),
53+
);
54+
} else {
55+
$fileData = new VideoMetadata(
56+
name: $getValue($row, 'filename'),
57+
lat: $this->maybeCastToFloat($getValue($row, 'lat')),
58+
lng: $this->maybeCastToFloat($getValue($row, 'lng')),
59+
takenAt: $takenAt,
60+
area: $this->maybeCastToFloat($getValue($row, 'area')),
61+
distanceToGround: $this->maybeCastToFloat($getValue($row, 'distance_to_ground')),
62+
gpsAltitude: $this->maybeCastToFloat($getValue($row, 'gps_altitude')),
63+
yaw: $this->maybeCastToFloat($getValue($row, 'yaw')),
64+
);
65+
66+
$data->addFile($fileData);
67+
}
68+
}
69+
70+
return $data;
71+
}
72+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Biigle\Services\MetadataParsing;
4+
5+
use Illuminate\Support\Collection;
6+
7+
class VideoMetadata extends FileMetadata
8+
{
9+
public Collection $frames;
10+
11+
public function __construct(
12+
public string $name,
13+
public ?float $lat = null,
14+
public ?float $lng = null,
15+
public ?string $takenAt = null,
16+
public ?float $area = null,
17+
public ?float $distanceToGround = null,
18+
public ?float $gpsAltitude = null,
19+
public ?float $yaw = null
20+
)
21+
{
22+
parent::__construct($name);
23+
24+
$this->frames = collect([]);
25+
26+
if (!is_null($takenAt)) {
27+
$this->addFrame(
28+
takenAt: $takenAt,
29+
lat: $lat,
30+
lng: $lng,
31+
area: $area,
32+
distanceToGround: $distanceToGround,
33+
gpsAltitude: $gpsAltitude,
34+
yaw: $yaw
35+
);
36+
}
37+
}
38+
39+
public function getFrames(): Collection
40+
{
41+
return $this->frames;
42+
}
43+
44+
public function addFrame(
45+
string $takenAt,
46+
?float $lat = null,
47+
?float $lng = null,
48+
?float $area = null,
49+
?float $distanceToGround = null,
50+
?float $gpsAltitude = null,
51+
?float $yaw = null
52+
): void
53+
{
54+
$frame = new ImageMetadata(
55+
name: $this->name,
56+
takenAt: $takenAt,
57+
lat: $lat,
58+
lng: $lng,
59+
area: $area,
60+
distanceToGround: $distanceToGround,
61+
gpsAltitude: $gpsAltitude,
62+
yaw: $yaw
63+
);
64+
$this->frames->push($frame);
65+
}
66+
67+
/**
68+
* Determines if any metadata field other than the name is filled.
69+
*/
70+
public function isEmpty(): bool
71+
{
72+
return $this->frames->isEmpty()
73+
&& is_null($this->lat)
74+
&& is_null($this->lng)
75+
&& is_null($this->takenAt)
76+
&& is_null($this->area)
77+
&& is_null($this->distanceToGround)
78+
&& is_null($this->gpsAltitude)
79+
&& is_null($this->yaw);
80+
}
81+
}

app/Services/MetadataParsing/VolumeMetadata.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public function getFiles(): Collection
2929
return $this->files->values();
3030
}
3131

32+
public function getFile(string $name): ?FileMetadata
33+
{
34+
return $this->files->get($name);
35+
}
36+
3237
/**
3338
* Determine if there is any file metadata.
3439
*/

0 commit comments

Comments
 (0)