diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/GpsPosition.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/GpsPosition.php new file mode 100644 index 0000000000..97efeea8cb --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/GpsPosition.php @@ -0,0 +1,99 @@ +longitude = (float) $value; + break; + + case self::LATITUDE_TAG_NAME: + Assertion::numeric($value); + $this->latitude = (float) $value; + break; + + case self::LONGITUDE_REF_TAG_NAME: + $normalized = strtoupper($value); + if ($normalized !== self::LONGITUDE_REF_EAST && $normalized !== self::LONGITUDE_REF_WEST) { + throw new \InvalidArgumentException(sprintf('Invalid longitude reference "%s" (expecting "%s" or "%s").', $value, self::LONGITUDE_REF_EAST, self::LONGITUDE_REF_WEST)); + } + $this->longitude_ref = $value; + break; + + case self::LATITUDE_REF_TAG_NAME: + $normalized = strtoupper($value); + if ($normalized !== self::LATITUDE_REF_NORTH && $normalized !== self::LATITUDE_REF_SOUTH) { + throw new \InvalidArgumentException(sprintf('Invalid latitude reference "%s" (expecting "%s" or "%s").', $value, self::LATITUDE_REF_NORTH, self::LATITUDE_REF_SOUTH)); + } + $this->latitude_ref = $normalized; + break; + + default: + throw new \InvalidArgumentException(sprintf('Unsupported tag name "%s".', $tag_name)); + } + } + + public static function isSupportedTagName($tag_name) + { + return in_array($tag_name, [ + self::LONGITUDE_TAG_NAME, + self::LONGITUDE_REF_TAG_NAME, + self::LATITUDE_TAG_NAME, + self::LATITUDE_REF_TAG_NAME + ]); + } + + public function isComplete() + { + return $this->longitude !== null + && $this->longitude_ref !== null + && $this->latitude !== null + && $this->latitude_ref !== null; + } + + public function getSignedLongitude() + { + if ($this->longitude === null) { + return null; + } + return $this->longitude * ($this->longitude_ref === self::LONGITUDE_REF_WEST ? -1 : 1); + } + + public function getSignedLatitude() + { + if ($this->latitude === null) { + return null; + } + return $this->latitude * ($this->latitude_ref === self::LATITUDE_REF_SOUTH ? -1 : 1); + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/MetadataHydrator.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/MetadataHydrator.php index 34fe519718..5785ecd2e1 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/MetadataHydrator.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/Record/Hydrator/MetadataHydrator.php @@ -19,6 +19,7 @@ use Alchemy\Phrasea\Utilities\StringHelper; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Connection as DriverConnection; use DomainException; +use InvalidArgumentException; class MetadataHydrator implements HydratorInterface { @@ -26,6 +27,8 @@ class MetadataHydrator implements HydratorInterface private $structure; private $helper; + private $gps_position_buffer = []; + public function __construct(DriverConnection $connection, Structure $structure, RecordHelper $helper) { $this->connection = $connection; @@ -93,12 +96,16 @@ SQL; break; case 'exif': - // EXIF data is single-valued + if (GpsPosition::isSupportedTagName($key)) { + $this->handleGpsPosition($records, $id, $key, $value); + break; + } $tag = $this->structure->getMetadataTagByName($key); if ($tag) { $value = $this->sanitizeValue($value, $tag->getType()); } - $record['exif'][$key] = $value; + // EXIF data is single-valued + $record['metadata_tags'][$key] = $value; break; default: @@ -106,6 +113,8 @@ SQL; break; } } + + $this->clearGpsPositionBuffer(); } private function sanitizeValue($value, $type) @@ -131,4 +140,26 @@ SQL; return $value; } } + + private function handleGpsPosition(&$records, $id, $tag_name, $value) + { + // Get position object + if (!isset($this->gps_position_buffer[$id])) { + $this->gps_position_buffer[$id] = new GpsPosition(); + } + $position = $this->gps_position_buffer[$id]; + // Push this tag into object + $position->set($tag_name, $value); + // Try to output complete position + if ($position->isComplete()) { + $records[$id]['metadata_tags']['Longitude'] = $position->getSignedLongitude(); + $records[$id]['metadata_tags']['Latitude'] = $position->getSignedLatitude(); + unset($this->gps_position_buffer[$id]); + } + } + + private function clearGpsPositionBuffer() + { + $this->gps_position_buffer = []; + } } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php index 8656543652..f39959cece 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer/RecordIndexer.php @@ -307,7 +307,7 @@ class RecordIndexer // Thesaurus ->add('concept_path', $this->getThesaurusPathMapping()) // EXIF - ->add('exif', $this->getMetadataTagMapping()) + ->add('metadata_tags', $this->getMetadataTagMapping()) // Status ->add('flags', $this->getFlagsMapping()) ->add('flags_bitfield', 'integer')->notIndexed() diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Tag.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Tag.php index 0a4895832e..93eabc1716 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Tag.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Structure/Tag.php @@ -39,7 +39,7 @@ class Tag implements Typed public function getIndexField($raw = false) { return sprintf( - 'exif.%s%s', + 'metadata_tags.%s%s', $this->name, $raw && $this->type === Mapping::TYPE_STRING ? '.raw' : '' ); diff --git a/lib/classes/media/subdef.php b/lib/classes/media/subdef.php index 3658b02061..f6af9b8b72 100644 --- a/lib/classes/media/subdef.php +++ b/lib/classes/media/subdef.php @@ -125,7 +125,9 @@ class media_subdef extends media_abstract implements cache_cacheableInterface const TC_DATA_MIMETYPE = 'MimeType'; const TC_DATA_FILESIZE = 'FileSize'; const TC_DATA_LONGITUDE = 'Longitude'; + const TC_DATA_LONGITUDE_REF = 'LongitudeRef'; const TC_DATA_LATITUDE = 'Latitude'; + const TC_DATA_LATITUDE_REF = 'LatitudeRef'; const TC_DATA_FOCALLENGTH = 'FocalLength'; const TC_DATA_CAMERAMODEL = 'CameraModel'; const TC_DATA_FLASHFIRED = 'FlashFired'; @@ -643,6 +645,10 @@ class media_subdef extends media_abstract implements cache_cacheableInterface self::TC_DATA_VIDEOCODEC => 'getVideoCodec', self::TC_DATA_AUDIOCODEC => 'getAudioCodec', self::TC_DATA_ORIENTATION => 'getOrientation', + self::TC_DATA_LONGITUDE => 'getLongitude', + self::TC_DATA_LONGITUDE_REF => 'getLongitudeRef', + self::TC_DATA_LATITUDE => 'getLatitude', + self::TC_DATA_LATITUDE_REF => 'getLatitudeRef', ]; foreach ($methods as $tc_name => $method) { @@ -655,8 +661,6 @@ class media_subdef extends media_abstract implements cache_cacheableInterface } } - $datas[self::TC_DATA_LONGITUDE] = $media->getLongitude(); - $datas[self::TC_DATA_LATITUDE] = $media->getLatitude(); $datas[self::TC_DATA_MIMETYPE] = $media->getFile()->getMimeType(); $datas[self::TC_DATA_FILESIZE] = $media->getFile()->getSize(); diff --git a/tests/Alchemy/Tests/Phrasea/SearchEngine/Indexer/GpsPositionTest.php b/tests/Alchemy/Tests/Phrasea/SearchEngine/Indexer/GpsPositionTest.php new file mode 100644 index 0000000000..54b37c37b4 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/SearchEngine/Indexer/GpsPositionTest.php @@ -0,0 +1,112 @@ +set('Latitude', 48.856578); + $this->assertFalse($position->isComplete()); + } + + public function testIsCompleteWithAllSet() + { + $position = new GpsPosition(); + $position->set('Latitude', 48.856578); + $position->set('LatitudeRef', 'N'); + $position->set('Longitude', 2.351828); + $position->set('LongitudeRef', 'E'); + $this->assertTrue($position->isComplete()); + } + + /** + * @dataProvider getSupportedTagNames + */ + public function testSupportedTagNames($tag_name) + { + $this->assertTrue(GpsPosition::isSupportedTagName($tag_name)); + } + + public function getSupportedTagNames() + { + return [ + ['Longitude'], + ['LongitudeRef'], + ['Latitude'], + ['LatitudeRef'], + [GpsPosition::LONGITUDE_TAG_NAME], + [GpsPosition::LONGITUDE_REF_TAG_NAME], + [GpsPosition::LATITUDE_TAG_NAME], + [GpsPosition::LATITUDE_REF_TAG_NAME] + ]; + } + + /** + * @dataProvider getUnsupportedTagNames + */ + public function testUnsupportedTagNames($tag_name) + { + $this->assertFalse(GpsPosition::isSupportedTagName($tag_name)); + } + + public function getUnsupportedTagNames() + { + return [ + ['foo'], + ['lOnGiTuDe'] + ]; + } + + public function testGetSignedLongitude() + { + $position = new GpsPosition(); + $position->set('Longitude', 2.351828); + $this->assertEquals(2.351828, $position->getSignedLongitude()); + + $position = new GpsPosition(); + $position->set('LongitudeRef', 'E'); + $this->assertNull($position->getSignedLongitude()); + + $position = new GpsPosition(); + $position->set('Longitude', 2.351828); + $position->set('LongitudeRef', 'E'); + $this->assertEquals(2.351828, $position->getSignedLongitude()); + + $position = new GpsPosition(); + $position->set('Longitude', 2.351828); + $position->set('LongitudeRef', 'W'); + $this->assertEquals(-2.351828, $position->getSignedLongitude()); + } + + public function testGetSignedLatitude() + { + $position = new GpsPosition(); + $position->set('Latitude', 48.856578); + $this->assertEquals(48.856578, $position->getSignedLatitude()); + + $position = new GpsPosition(); + $position->set('LatitudeRef', 'N'); + $this->assertNull($position->getSignedLatitude()); + + $position = new GpsPosition(); + $position->set('Latitude', 48.856578); + $position->set('LatitudeRef', 'N'); + $this->assertEquals(48.856578, $position->getSignedLatitude()); + + $position = new GpsPosition(); + $position->set('Latitude', 48.856578); + $position->set('LatitudeRef', 'S'); + $this->assertEquals(-48.856578, $position->getSignedLatitude()); + } +}