Support geo located images & videos

This commit is contained in:
2023-08-12 23:35:40 +02:00
parent 199e3a1269
commit 5f597647b4
4 changed files with 140 additions and 24 deletions

View File

@@ -0,0 +1,3 @@
ALTER TABLE medias ADD latitude DECIMAL(7,5) AFTER timezone;
ALTER TABLE medias ADD longitude DECIMAL(8,5) AFTER latitude;
ALTER TABLE medias ADD altitude SMALLINT AFTER longitude;

View File

@@ -76,7 +76,7 @@ class Media extends PhpObject {
if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) { if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) {
if($this->oProject->getProjectId()) { if($this->oProject->getProjectId()) {
$asParams = array( $asParams = array(
'select' => array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'width', 'height', 'rotate', 'type AS subtype', 'comment'), 'select' => array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'width', 'height', 'rotate', 'type AS subtype', 'comment'),
'from' => self::MEDIA_TABLE, 'from' => self::MEDIA_TABLE,
'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()) 'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId())
); );
@@ -128,6 +128,9 @@ class Media extends PhpObject {
'taken_on' => date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']), 'taken_on' => date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']),
'posted_on' => date(Db::TIMESTAMP_FORMAT, $asMediaInfo['file_ts']), 'posted_on' => date(Db::TIMESTAMP_FORMAT, $asMediaInfo['file_ts']),
'timezone' => $asMediaInfo['timezone'], 'timezone' => $asMediaInfo['timezone'],
'latitude' => $asMediaInfo['latitude'],
'longitude' => $asMediaInfo['longitude'],
'altitude' => $asMediaInfo['altitude'],
'width' => $asMediaInfo['width'], 'width' => $asMediaInfo['width'],
'height' => $asMediaInfo['height'], 'height' => $asMediaInfo['height'],
'rotate' => $asMediaInfo['rotate'], 'rotate' => $asMediaInfo['rotate'],
@@ -151,21 +154,23 @@ class Media extends PhpObject {
{ {
$sMediaPath = self::getMediaPath($sMediaName); $sMediaPath = self::getMediaPath($sMediaName);
$sType = self::getMediaType($sMediaName); $sType = self::getMediaType($sMediaName);
$iPostedOn = filemtime($sMediaPath); $iPostedOn = filemtime($sMediaPath);
$sTimeZone = date_default_timezone_get(); $sTimeZone = date_default_timezone_get();
$iWidth = 0; $iWidth = 0;
$iHeight = 0; $iHeight = 0;
$sRotate = '0'; $sRotate = '0';
$sTakenOn = ''; $sTakenOn = '';
$fLat = 0;
$fLng = 0;
$iAlt = 0;
switch($sType) { switch($sType) {
case 'video': case 'video':
$asResult = array(); $asResult = array();
$sParams = implode(' ', array( $sParams = implode(' ', array(
'-loglevel error', //Remove comments '-loglevel error', //Remove comments
'-select_streams v:0', //First video channel '-select_streams v:0', //First video channel
'-show_entries '. //filter tags : Width, Height, Creation Time & Rotation '-show_entries '. //filter tags : Width, Height, Creation Time, Location & Rotation
'format_tags=creation_time,com.apple.quicktime.creationdate:'. 'format_tags=creation_time,com.apple.quicktime.creationdate,com.apple.quicktime.location.ISO6709:'.
'stream_tags=rotate,creation_time:'. 'stream_tags=rotate,creation_time:'.
'stream=width,height', 'stream=width,height',
'-print_format json', //output format: json '-print_format json', //output format: json
@@ -181,6 +186,11 @@ class Media extends PhpObject {
} }
else $sTakenOn = $asExif['format']['tags']['creation_time'] ?? $asExif['streams'][0]['tags']['creation_time']; else $sTakenOn = $asExif['format']['tags']['creation_time'] ?? $asExif['streams'][0]['tags']['creation_time'];
//Location
if(isset($asExif['format']['tags']['com.apple.quicktime.location.ISO6709'])) {
list($fLat, $fLng, $iAlt) = self::getLatLngAltFromISO6709($asExif['format']['tags']['com.apple.quicktime.location.ISO6709']);
}
//Width & Height //Width & Height
$iWidth = $asExif['streams'][0]['width']; $iWidth = $asExif['streams'][0]['width'];
$iHeight = $asExif['streams'][0]['height']; $iHeight = $asExif['streams'][0]['height'];
@@ -209,6 +219,14 @@ class Media extends PhpObject {
$sTimeZone = $asExif['EXIF']['OffsetTimeOriginal'] ?? $asExif['EXIF']['UndefinedTag:0x9011'] ?? Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone; $sTimeZone = $asExif['EXIF']['OffsetTimeOriginal'] ?? $asExif['EXIF']['UndefinedTag:0x9011'] ?? Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
} }
//Location
if(array_key_exists('GPS', $asExif)) {
$asGps = $asExif['GPS'];
$fLat = self::getLatLngFromExif($asGps['GPSLatitudeRef'] ?? $asGps['UndefinedTag:0x0001'], $asGps['GPSLatitude'] ?? $asGps['UndefinedTag:0x0002']);
$fLng = self::getLatLngFromExif($asGps['GPSLongitudeRef'] ?? $asGps['UndefinedTag:0x0003'], $asGps['GPSLongitude'] ?? $$asGps['UndefinedTag:0x0004']);
$iAlt = (($asGps['GPSAltitudeRef'] ?? $asGps['UndefinedTag:0x0005'] ?? '0') == '1'?-1:1) * ($asGps['GPSAltitude'] ?? $asGps['UndefinedTag:0x0006'] ?? 0);
}
//Orientation //Orientation
if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) { if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) {
switch($asExif['IFD0']['Orientation']) switch($asExif['IFD0']['Orientation'])
@@ -232,6 +250,9 @@ class Media extends PhpObject {
return array( return array(
'timezone' => $sTimeZone, 'timezone' => $sTimeZone,
'latitude' => $fLat,
'longitude' => $fLng,
'altitude' => $iAlt,
'taken_ts' => $iTakenOn, 'taken_ts' => $iTakenOn,
'file_ts' => $iPostedOn, 'file_ts' => $iPostedOn,
'width' => $iWidth, 'width' => $iWidth,
@@ -290,4 +311,29 @@ class Media extends PhpObject {
return $sType; return $sType;
} }
}
private static function getLatLngFromExif($sDirection, $asCoords) {
$fCoord = 0;
foreach($asCoords as $iIndex=>$sCoord) {
$fValue = 0;
$asCoordParts = explode('/', $sCoord);
switch(count($asCoordParts)) {
case 1:
$fValue = $asCoordParts[0];
break;
case 2:
$fValue = floatval($asCoordParts[0]) / floatval($asCoordParts[1]);
break;
}
$fCoord += $fValue / pow(60, $iIndex);
}
return $fCoord * (($sDirection == 'W' || $sDirection == 'S')?-1:1);
}
private static function getLatLngAltFromISO6709($sIso6709) {
preg_match('/^(?P<lat>[\+\-][0,1]?\d{2}\.\d+)(?P<lng>[\+\-][0,1]?\d{2}\.\d+)(?P<alt>[\+\-]\d+)?/', $sIso6709, $asMatches);
return array(floatval($asMatches['lat']), floatval($asMatches['lng']), floatval($asMatches['alt'] ?? 0));
}
}

View File

@@ -87,7 +87,7 @@ class Spot extends Main
Feed::SPOT_TABLE => array('ref_spot_id', 'name', 'model'), Feed::SPOT_TABLE => array('ref_spot_id', 'name', 'model'),
Project::PROJ_TABLE => array('name', 'codename', 'active_from', 'active_to'), Project::PROJ_TABLE => array('name', 'codename', 'active_from', 'active_to'),
self::POST_TABLE => array(Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone'), self::POST_TABLE => array(Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time', 'timezone'),
Media::MEDIA_TABLE => array(Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'width', 'height', 'rotate', 'comment'), Media::MEDIA_TABLE => array(Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'comment'),
User::USER_TABLE => array('name', 'email', 'gravatar', 'language', 'timezone', 'active', 'clearance'), User::USER_TABLE => array('name', 'email', 'gravatar', 'language', 'timezone', 'active', 'clearance'),
Map::MAP_TABLE => array('codename', 'pattern', 'token', 'tile_size', 'min_zoom', 'max_zoom', 'attribution'), Map::MAP_TABLE => array('codename', 'pattern', 'token', 'tile_size', 'min_zoom', 'max_zoom', 'attribution'),
Map::MAPPING_TABLE => array(Db::getId(Map::MAP_TABLE) , Db::getId(Project::PROJ_TABLE)) Map::MAPPING_TABLE => array(Db::getId(Map::MAP_TABLE) , Db::getId(Project::PROJ_TABLE))
@@ -110,6 +110,7 @@ class Spot extends Main
'last_update' => "TIMESTAMP DEFAULT 0", 'last_update' => "TIMESTAMP DEFAULT 0",
'latitude' => "DECIMAL(7,5)", 'latitude' => "DECIMAL(7,5)",
'longitude' => "DECIMAL(8,5)", 'longitude' => "DECIMAL(8,5)",
'altitude' => "SMALLINT",
'model' => "VARCHAR(20)", 'model' => "VARCHAR(20)",
'name' => "VARCHAR(100)", 'name' => "VARCHAR(100)",
'pattern' => "VARCHAR(200) NOT NULL", 'pattern' => "VARCHAR(200) NOT NULL",
@@ -121,7 +122,7 @@ class Spot extends Main
'site_time' => "TIMESTAMP DEFAULT 0", //DEFAULT 0 removes auto-set to current time 'site_time' => "TIMESTAMP DEFAULT 0", //DEFAULT 0 removes auto-set to current time
'status' => "VARCHAR(10)", 'status' => "VARCHAR(10)",
'taken_on' => "TIMESTAMP DEFAULT 0", 'taken_on' => "TIMESTAMP DEFAULT 0",
'timezone' => "CHAR(64) NOT NULL", //see mysql.time_zone_name 'timezone' => "CHAR(64) NOT NULL", //see mysql.time_zone_name
'token' => "VARCHAR(4096)", 'token' => "VARCHAR(4096)",
'type' => "VARCHAR(20)", 'type' => "VARCHAR(20)",
'unix_time' => "INT", 'unix_time' => "INT",
@@ -269,23 +270,24 @@ class Spot extends Main
public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false) public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false)
{ {
$asMessages = $this->getSpotMessages($asMessageIds); $asMessages = $this->getSpotMessages($asMessageIds);
$asGeoMedias = array();
usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
$bHasMsg = !empty($asMessages);
//Add medias //Add medias
if(!empty($asMessages)) { $asMedias = $this->getMedias('taken_on', $asMediaIds);
$asMedias = $this->getMedias('taken_on', $asMediaIds); usort($asMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
usort($asMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
//Assign medias to closest message //Assign medias to closest message
$iIndex = 0; $iIndex = 0;
$iMaxIndex = count($asMessages) - 1; $iMaxIndex = count($asMessages) - 1;
foreach($asMedias as $asMedia) { foreach($asMedias as $asMedia) {
while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) { if($asMedia['latitude']!='' && $asMedia['longitude']!='') $asGeoMedias[] = $asMedia;
$iIndex++; elseif($bHasMsg) {
} while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) $iIndex++;
//All medias before first message or after last message are assigned to first/last message respectively //All medias before first message or after last message are assigned to first/last message respectively
if($iIndex == 0) $iMsgIndex = $iIndex; if($iIndex == 0) $iMsgIndex = $iIndex;
elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex; elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex;
else { else {
$iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2; $iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2;
@@ -302,6 +304,7 @@ class Spot extends Main
$asResult = array( $asResult = array(
'messages' => $asMessages, 'messages' => $asMessages,
'medias' => $asGeoMedias,
'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()), 'maps' => $this->oMap->getProjectMaps($this->oProject->getProjectId()),
'last_update' => $asLastUpdate 'last_update' => $asLastUpdate
); );

View File

@@ -282,7 +282,7 @@ function initProject(sProjectCodeName, oFocusPost){
).done(function(aoMessages, aoTracks) { ).done(function(aoMessages, aoTracks) {
var asData = aoMessages[0]['data']; var asData = aoMessages[0]['data'];
setMapLayers(asData['maps']); setMapLayers(asData['maps']);
initSpotMessages(asData['messages'], aoTracks[0]); initSpotMessages(asData['messages'], aoTracks[0], asData['medias']);
updateSettingsPanel(asData['last_update']); updateSettingsPanel(asData['last_update']);
}); });
@@ -420,7 +420,7 @@ function setMapLayers(asLayers) {
}); });
} }
function initSpotMessages(aoMessages, aoTracks) { function initSpotMessages(aoMessages, aoTracks, aoMedias) {
var bIsMobile = isMobile(); var bIsMobile = isMobile();
//Map //Map
@@ -656,7 +656,10 @@ function initSpotMessages(aoMessages, aoTracks) {
} }
//Add Spot messages //Add Spot messages
addSpotMessages(aoMessages); addSpotMarkers(aoMessages);
//Add Medias
addMediaMarkers(aoMedias)
//Open tooltip on latest message in mobile mode //Open tooltip on latest message in mobile mode
if( if(
@@ -672,7 +675,7 @@ function initSpotMessages(aoMessages, aoTracks) {
*/ */
} }
function addSpotMessages(aoMessages) { function addSpotMarkers(aoMessages) {
//Spot Messages //Spot Messages
var iWorkSpaceMinWidth = isMobile()?self.tmp('$Projects').width():(self.tmp('$Projects').width() - self.tmp('$Feed').outerWidth(true) - self.tmp('$Settings').outerWidth(true)); var iWorkSpaceMinWidth = isMobile()?self.tmp('$Projects').width():(self.tmp('$Projects').width() - self.tmp('$Feed').outerWidth(true) - self.tmp('$Settings').outerWidth(true));
@@ -689,7 +692,7 @@ function addSpotMessages(aoMessages) {
$Tooltip = $('<div>', {'class':'info-window'}) $Tooltip = $('<div>', {'class':'info-window'})
.append($('<h1>') .append($('<h1>')
.addIcon('fa-message fa-lg', true) .addIcon('fa-message fa-lg', true)
.append($('<span>').text('Message '+oSpot.lang('counter', oMsg.displayed_id))) .append($('<span>').text(oSpot.lang('post_message')+' '+oSpot.lang('counter', oMsg.displayed_id)))
.append($('<span>', {'class':'message-type'}).text('('+oMsg.type+')')) .append($('<span>', {'class':'message-type'}).text('('+oMsg.type+')'))
) )
.append($('<div>', {'class':'separator'})) .append($('<div>', {'class':'separator'}))
@@ -746,6 +749,67 @@ function addSpotMessages(aoMessages) {
}); });
} }
function addMediaMarkers(aoMedias) {
var iWorkSpaceMinWidth = isMobile()?self.tmp('$Projects').width():(self.tmp('$Projects').width() - self.tmp('$Feed').outerWidth(true) - self.tmp('$Settings').outerWidth(true));
$.each(aoMedias, function(iKey, oMedia){
var oMarker = L.marker(L.latLng(oMedia.latitude, oMedia.longitude), {
id: oMedia.id_media,
riseOnHover: true,
icon: getDivIcon(oMedia.subtype+' fa-rotate-270')
}).addTo(self.tmp('map'));
//Tooltip
$Tooltip = $('<div>', {'class':'info-window'})
.append($('<h1>')
.addIcon('fa-'+oMedia.subtype+' fa-lg', true)
.append($('<span>').text(oSpot.lang(oMedia.subtype)+' '+oSpot.lang('counter', oMedia.displayed_id || oMedia.id_media)))
)
.append($('<div>', {'class':'separator'}))
.append($('<p>', {'class':'time'})
.addIcon('fa-time fa-fw fa-lg', true)
.append(oMedia.formatted_time+(self.vars(['project', 'mode'])==self.consts.modes.blog?' ('+oMedia.relative_time+')':'')));
//Tooltip: Time Zone
if(oMedia.formatted_time_local != oMedia.formatted_time) {
$Tooltip.append($('<p>', {'class':'timezone'})
.addIcon('fa-timezone fa-fw fa-lg', true)
.append(oSpot.lang('local_time', getRelativeTime(oMedia.formatted_time_local, oMedia.day_offset))));
}
$Tooltip.data('medias', [oMedia]);
oSpot.tmp(['marker-media-tooltips', oMedia.id_media], $Tooltip);
oMarker.bindPopup(
function(e) {
let $Tooltip = oSpot.tmp(['marker-media-tooltips', e.options.id]);
$Tooltip.on('mouseout', function(){e.closePopup();});
//Tooltip: Medias: Set on the fly to avoid resource load
let oMedias = $Tooltip.data('medias');
let $Medias = $Tooltip.find('.medias');
if(oMedias && $Medias.length == 0) {
$Medias = $('<div>', {'class':'medias'});
$.each(oMedias, function(iKey, asMedia) {
$Medias.append(getMediaLink(asMedia, 'marker'));
});
$Tooltip.append($Medias);
}
return $Tooltip[0];
},
{
maxWidth: iWorkSpaceMinWidth,
autoPan: false,
closeOnClick: true,
offset: new L.Point(0, -30)
}
);
oMarker.on('mouseover', function(e) {this.openPopup();});
});
}
function toggleSoftPopup(e, sMode) { function toggleSoftPopup(e, sMode) {
switch(sMode) { switch(sMode) {
case 'open': case 'open':
@@ -806,7 +870,7 @@ function checkNewFeed() {
self.tmp('$PostList').prepend($Posts.children()); self.tmp('$PostList').prepend($Posts.children());
//Markers //Markers
addSpotMessages(asData.messages); addSpotMarkers(asData.messages);
//Message Last Update //Message Last Update
updateSettingsPanel(asData.last_update); updateSettingsPanel(asData.last_update);