diff --git a/inc/Media.php b/inc/Media.php index a1da486..c15d029 100644 --- a/inc/Media.php +++ b/inc/Media.php @@ -69,16 +69,19 @@ class Media extends PhpObject { return Spot::getResult(($sError==''), $sError, $asData); } - public function getMediasInfo($iMediaId=0) { - $bOwnMedia = ($iMediaId > 0); - if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias)) { + public function getMediasInfo($oMediaIds=null) { + $bOwnMedia = is_numeric($oMediaIds); //1 value + $bConstraintArray = is_array($oMediaIds); //Custom Constraints + + if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) { if($this->oProject->getProjectId()) { $asParams = array( 'select' => array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'rotate', 'type AS subtype', 'comment'), 'from' => self::MEDIA_TABLE, 'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()) ); - if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $iMediaId; + if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $oMediaIds; + if($bConstraintArray) $asParams = array_merge($asParams, $oMediaIds); $asMedias = $this->oDb->selectRows($asParams); foreach($asMedias as &$asMedia) { @@ -86,13 +89,13 @@ class Media extends PhpObject { $asMedia['thumb_path'] = $this->getMediaThumbnail($asMedia['filename']); } - if(!empty($asMedias)) { + if(!empty($asMedias) && !$bConstraintArray) { if($bOwnMedia) $this->asMedia = array_shift($asMedias); else $this->asMedias = $asMedias; } } } - return $bOwnMedia?$this->asMedia:$this->asMedias; + return $bOwnMedia?$this->asMedia:$asMedias; } public function getInfo() { diff --git a/inc/Project.php b/inc/Project.php index 45d4132..98b2d6e 100644 --- a/inc/Project.php +++ b/inc/Project.php @@ -164,6 +164,17 @@ class Project extends PhpObject { return $iLastUpdate; } + public function getLastMessageId($asConstraints=array()): int { + $iLastMsg = 0; + + $asFeedIds = $this->getFeedIds(); + foreach($asFeedIds as $iFeedId) { + $iLastMsg = max($iLastMsg, (new Feed($this->oDb, $iFeedId))->getLastMessageId($asConstraints)); + } + + return $iLastMsg; + } + public function getMaps() { $sQuery = "SELECT codename, geo_name, min_zoom, max_zoom, attribution ". diff --git a/inc/Spot.php b/inc/Spot.php index bbc1121..b43659d 100755 --- a/inc/Spot.php +++ b/inc/Spot.php @@ -88,7 +88,7 @@ class Spot extends Main ( 'tables' => array ( - Feed::MSG_TABLE => array('ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state'), + Feed::MSG_TABLE => array('ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'timezone', 'unix_time', 'content', 'battery_state', 'posted_on'), Feed::FEED_TABLE => array('ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'), Feed::SPOT_TABLE => array('ref_spot_id', 'name', 'model'), Project::PROJ_TABLE => array('name', 'codename', 'active_from', 'active_to'), @@ -146,7 +146,10 @@ class Spot extends Main ), 'cascading_delete' => array ( - Feed::SPOT_TABLE => array(Feed::MSG_TABLE) + Feed::SPOT_TABLE => array(Feed::FEED_TABLE), + Feed::FEED_TABLE => array(Feed::MSG_TABLE), + Project::PROJ_TABLE => array(Feed::FEED_TABLE, Media::MEDIA_TABLE, self::POST_TABLE), + self::MAP_TABLE => array(self::MAPPING_TABLE) ) ); } @@ -217,14 +220,13 @@ class Spot extends Main $oEmail->setDestInfo($this->oUser->getActiveUsersInfo()); //Add Position - $asMessages = $this->getSpotMessages(); - $asLastMessage = end($asMessages); + $asLastMessage = array_shift($this->getSpotMessages(array($this->oProject->getLastMessageId($this->getFeedConstraints(Feed::MSG_TABLE))))); $asLastMessage['token'] = Settings::GEO_SERVER_TOKEN; $oEmail->oTemplate->setTags($asLastMessage); $oEmail->oTemplate->setTag('date_time', 'time:'.$asLastMessage['unix_time'], 'd/m/Y, H:i'); //Add latest news feed - $asNews = $this->getNewsFeed(0, 0, true); + $asNews = $this->getNextFeed(0, true); $iPostCount = 0; foreach($asNews as $asPost) { if($asPost['type'] != 'message') { @@ -262,13 +264,15 @@ class Spot extends Main return self::getJsonResult($bSuccess, ''); } - public function getMarkers() + public function getMarkers($asMessageIds=array(), $asMediaIds=array(), $bInternal=false) { - $asMessages = $this->getSpotMessages(); + $asMessages = $this->getSpotMessages($asMessageIds); + usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); //Add medias if(!empty($asMessages)) { - $asMedias = $this->getMedias('taken_on'); + $asMedias = $this->getMedias('taken_on', $asMediaIds); + usort($asMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); //Assign medias to closest message $iIndex = 0; @@ -277,7 +281,9 @@ class Spot extends Main while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) { $iIndex++; } - if($iIndex == 0) $iMsgIndex = $iIndex; + + //All medias before first message or after last message are assigned to first/last message respectively + if($iIndex == 0) $iMsgIndex = $iIndex; elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex; else { $iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2; @@ -288,15 +294,17 @@ class Spot extends Main } } - //Add Project Last Update + //Spot Last Update $asLastUpdate = array(); $this->addTimeStamp($asLastUpdate, $this->oProject->getLastUpdate()); - return self::getJsonResult(true, '', array( + $asResult = array( 'messages' => $asMessages, 'maps' => $this->oProject->getMaps(), 'last_update' => $asLastUpdate - )); + ); + + return $bInternal?$asResult:self::getJsonResult(true, '', $asResult); } public function subscribe($sEmail) { @@ -327,34 +335,35 @@ class Spot extends Main return $this->oLang->getTranslation($sDesc); } - private function getSpotMessages() + private function getSpotMessages($asMsgIds=array()) { - $asMessages = array(); + $asConstraints = $this->getFeedConstraints(Feed::MSG_TABLE); + if(!empty($asMsgIds)) { + $asConstraints['constraint'][Db::getId(Feed::MSG_TABLE)] = $asMsgIds; + $asConstraints['constOpe'][Db::getId(Feed::MSG_TABLE)] = 'IN'; + } + + $asCombinedMessages = array(); //Get messages from all feeds belonging to the project $asFeeds = $this->oProject->getFeedIds(); foreach($asFeeds as $iFeedId) { $oFeed = new Feed($this->oDb, $iFeedId); - $asMessages = $oFeed->getMessages($this->oProject->getActivePeriod()); - foreach($asMessages as &$asMessage) + $asMessages = $oFeed->getMessages($asConstraints); + foreach($asMessages as $asMessage) { $asMessage['latitude'] = floatval($asMessage['latitude']); $asMessage['longitude'] = floatval($asMessage['longitude']); $asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat'); $asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon'); + $asMessage['displayed_id'] = $asMessage[Db::getId(Feed::MSG_TABLE)]; $this->addTimeStamp($asMessage, $asMessage['unix_time'], $asMessage['timezone']); + $asCombinedMessages[] = $asMessage; } } - //Sort chronologically - usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); - - //Add Display ID - $asSortedMessages = array_values($asMessages); - foreach($asSortedMessages as $iIndex=>&$asSortedMessage) $asSortedMessage['displayed_id'] = $iIndex + 1; - - return $asSortedMessages; + return $asCombinedMessages; } /** @@ -364,48 +373,44 @@ class Spot extends Main * @param String $sTimeRefField Field to calculate relative times: 'taken_on' or 'posted_on' * @return Array Medias info */ - private function getMedias($sTimeRefField) + private function getMedias($sTimeRefField, $asMediaIds=array()) { - $asMedias = $this->oMedia->getMediasInfo(); - $asValidMedias = array(); - foreach($asMedias as $asMedia) { - $sTimeRef = $asMedia[$sTimeRefField]; - if($sTimeRef >= $this->oProject->getActivePeriod('from') && $sTimeRef <= $this->oProject->getActivePeriod('to')) { - $iTimeStampTakenOn = strtotime($asMedia['taken_on']); - $iTimeStampPostedOn = strtotime($asMedia['posted_on']); - $asMedia['taken_on_formatted'] = $this->getTimeFormat($iTimeStampTakenOn); - $asMedia['taken_on_formatted_local'] = $this->getTimeFormat($iTimeStampTakenOn, $asMedia['timezone']); - $asMedia['posted_on_formatted'] = $this->getTimeFormat($iTimeStampPostedOn); - $asMedia['posted_on_formatted_local'] = $this->getTimeFormat($iTimeStampPostedOn, $asMedia['timezone']); - - $this->addTimeStamp($asMedia, strtotime($sTimeRef), $asMedia['timezone']); - $asValidMedias[] = $asMedia; - } + //Constraints + $asConstraints = $this->getFeedConstraints(Media::MEDIA_TABLE, $sTimeRefField); + if(!empty($asMediaIds)) { + $asConstraints['constraint'][Db::getId(Media::MEDIA_TABLE)] = $asMediaIds; + $asConstraints['constOpe'][Db::getId(Media::MEDIA_TABLE)] = 'IN'; } - usort($asValidMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); + $asMedias = $this->oMedia->getMediasInfo($asConstraints); + foreach($asMedias as &$asMedia) { + $iTimeStampTakenOn = strtotime($asMedia['taken_on']); + $iTimeStampPostedOn = strtotime($asMedia['posted_on']); + $asMedia['taken_on_formatted'] = $this->getTimeFormat($iTimeStampTakenOn); + $asMedia['taken_on_formatted_local'] = $this->getTimeFormat($iTimeStampTakenOn, $asMedia['timezone']); + $asMedia['posted_on_formatted'] = $this->getTimeFormat($iTimeStampPostedOn); + $asMedia['posted_on_formatted_local'] = $this->getTimeFormat($iTimeStampPostedOn, $asMedia['timezone']); + $asMedia['displayed_id'] = $asMedia[Db::getId(Media::MEDIA_TABLE)]; - $asSortedMedias = array_values($asValidMedias); - foreach($asSortedMedias as $iIndex=>&$asSortedMedia) $asSortedMedia['displayed_id'] = $iIndex + 1; + $this->addTimeStamp($asMedia, strtotime($asMedia[$sTimeRefField]), $asMedia['timezone']); + } - return $asSortedMedias; + return $asMedias; } - private function getPosts() + private function getPosts($asPostIds=array()) { $asInfo = array( 'select' => array(Db::getFullColumnName(self::POST_TABLE, '*'), 'gravatar'), 'from' => self::POST_TABLE, - 'join' => array(User::USER_TABLE => Db::getId(User::USER_TABLE)), - 'constraint'=> array( - Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(), - 'site_time' => $this->oProject->getActivePeriod('to') - ), - 'constOpe' => array( - Db::getId(Project::PROJ_TABLE) => "=", - 'site_time' => "<=" - ) + 'join' => array(User::USER_TABLE => Db::getId(User::USER_TABLE)) ); + $asInfo = array_merge($asInfo, $this->getFeedConstraints(self::POST_TABLE)); + + if(!empty($asPostIds)) { + $asInfo['constraint'][Db::getId(self::POST_TABLE)] = $asPostIds; + $asInfo['constOpe'][Db::getId(self::POST_TABLE)] = 'IN'; + } $asPosts = $this->oDb->selectRows($asInfo); foreach($asPosts as &$asPost) { @@ -415,7 +420,6 @@ class Spot extends Main $this->addTimeStamp($asPost, $iUnixTimeStamp, $asPost['timezone']); } - usort($asPosts, function($a, $b){return $a['unix_time'] > $b['unix_time'];}); return $asPosts; } @@ -427,54 +431,145 @@ class Spot extends Main if($sTimeZone != '') $asData['formatted_time_local'] = $this->getTimeFormat($iTime, $sTimeZone); } - public function getNewsFeed($iChunk=0, $iRefTimePoint=0, $bInternal=false) - { - $bHistoMode = ($this->oProject->getMode() == Project::MODE_HISTO); - $asFeeds = array(); - $asFeedTypes = array( - 'message' => array( - 'table' => Feed::MSG_TABLE, - 'feed' => $this->getSpotMessages(), - 'priority' => 0 - ), - 'media' => array( - 'table' => Media::MEDIA_TABLE, - 'feed' => $this->getMedias('posted_on'), - 'priority' => 1 - ), - 'post' => array( - 'table' => self::POST_TABLE, - 'feed' => $this->getPosts(), - 'priority' => 2 - ) + private function getFeedConstraints($sType, $sTimeField='site_time', $sReturnFormat='array') { + $asConsArray = array(); + $sConsSql = ""; + $asActPeriod = $this->oProject->getActivePeriod(); + + //Filter on Project ID + $sConsSql = "WHERE ".Db::getId(Project::PROJ_TABLE)." = ".$this->oProject->getProjectId(); + $asConsArray = array( + 'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()), + 'constOpe' => array(Db::getId(Project::PROJ_TABLE) => "=") ); - foreach($asFeedTypes as $sFeedType=>$asFeedTypeInfo) { - foreach($asFeedTypeInfo['feed'] as $asFeed) { - $iTableId = $asFeed[Db::getId($asFeedTypeInfo['table'])]; - //Build unique sorting ID - $iSort = $asFeed['unix_time'].'.'.$asFeedTypeInfo['priority'].$iTableId; - - //Build feed (not including new feeds added in between chunk by concurrent access) - if($iChunk == 0 || $iSort <= $iRefTimePoint) { - $asFeeds[$iSort] = $asFeed; - $asFeeds[$iSort]['type'] = $sFeedType; - $asFeeds[$iSort]['id'] = $iTableId; - } - - //Build Reference Time Point (latest item extracted by first chunk) - if($iChunk == 0 && $iSort > $iRefTimePoint) $iRefTimePoint = $iSort; - } + //Time Filter + switch($sType) { + case Feed::MSG_TABLE: + $asConsArray['constraint'][$sTimeField] = $asActPeriod; + $asConsArray['constOpe'][$sTimeField] = "BETWEEN"; + $sConsSql .= " AND ".$sTimeField." BETWEEN '".$asActPeriod['from']."' AND '".$asActPeriod['to']."'"; + break; + case Media::MEDIA_TABLE: + $asConsArray['constraint'][$sTimeField] = $asActPeriod['to']; + $asConsArray['constOpe'][$sTimeField] = "<="; + $sConsSql .= " AND ".$sTimeField." <= '".$asActPeriod['to']."'"; + break; + case self::POST_TABLE: + $asConsArray['constraint'][$sTimeField] = $asActPeriod['to']; + $asConsArray['constOpe'][$sTimeField] = "<="; + $sConsSql .= " AND ".$sTimeField." <= '".$asActPeriod['to']."'"; + break; } - //Sort by key - if($bHistoMode) ksort($asFeeds); //Old first - else krsort($asFeeds); //New first + return ($sReturnFormat=='array')?$asConsArray:$sConsSql; + } - //Split chunks - $asFeeds = array_slice($asFeeds, $iChunk * self::FEED_CHUNK_SIZE, self::FEED_CHUNK_SIZE); + public function getNewFeed($iRefIdFirst) { + $asResult = array(); - return $bInternal?$asFeeds:self::getJsonResult(true, '', array('ref_id'=>$iRefTimePoint, 'feed' => $asFeeds)); + if($this->oProject->getMode() != Project::MODE_HISTO) { + $asMessageIds = $asMediaIds = array(); + + //New Feed Items + $asResult = $this->getFeed($iRefIdFirst, ">", "DESC"); + foreach($asResult['feed'] as $asItem) { + switch($asItem['type']) { + case 'message': + $asMessageIds[] = $asItem['id']; + break; + case 'media': + $asMediaIds[] = $asItem['id']; + break; + } + } + + //New Markers + $asMarkers = $this->getMarkers( + empty($asMessageIds)?array(0):$asMessageIds, + empty($asMediaIds)?array(0):$asMediaIds, + true + ); + + $asResult = array_merge($asResult, $asMarkers); + } + + return self::getJsonResult(true, '', $asResult); + } + + public function getNextFeed($iRefIdLast=0, $bInternal=false) { + if($this->oProject->getMode() == Project::MODE_HISTO) { + $sDirection = ">"; + $sSort = "ASC"; + } + else { + $sDirection = "<"; + $sSort = "DESC"; + } + $asResult = $this->getFeed($iRefIdLast, $sDirection, $sSort); + return $bInternal?$asResult['feed']:self::getJsonResult(true, '', $asResult); + } + + public function getFeed($iRefId=0, $sDirection, $sSort) { + $this->oDb->cleanSql($iRefId); + $this->oDb->cleanSql($sDirection); + $this->oDb->cleanSql($sSort); + + $sMediaRefField = 'posted_on'; + $sQuery = implode(" ", array( + "SELECT type, id, ref", + "FROM (", + "SELECT id_project, id_message AS id, 'message' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.0', id_message) AS ref", + "FROM ".Feed::MSG_TABLE, + "INNER JOIN feeds USING(id_feed)", + $this->getFeedConstraints(Feed::MSG_TABLE, 'site_time', 'sql'), + "UNION", + "SELECT id_project, id_media AS id, 'media' AS type, CONCAT(UNIX_TIMESTAMP(".$sMediaRefField."), '.1', id_media) AS ref", + "FROM ".Media::MEDIA_TABLE, + $this->getFeedConstraints(Media::MEDIA_TABLE, $sMediaRefField, 'sql'), + "UNION", + "SELECT id_project, id_post AS id, 'post' AS type, CONCAT(UNIX_TIMESTAMP(site_time), '.2', id_post) AS ref", + "FROM ".self::POST_TABLE, + $this->getFeedConstraints(self::POST_TABLE, 'site_time', 'sql'), + ") AS items", + ($iRefId > 0)?("WHERE ref ".$sDirection." ".$iRefId):"", + "ORDER BY ref ".$sSort, + "LIMIT ".self::FEED_CHUNK_SIZE + )); + + //Get new chunk + $asItems = $this->oDb->getArrayQuery($sQuery, true); + + //Update Reference Point with latest/earliest value + $iRefIdFirst = $iRefIdLast = 0; + if(!empty($asItems)) { + $iRefIdLast = end($asItems)['ref']; + $iRefIdFirst = reset($asItems)['ref']; + } + + //Sort Table IDs by type & Get attributes + foreach($asItems as $asItem) { + $asFeedIds[$asItem['type']][$asItem['id']] = $asItem; + } + $asFeedAttrs = array( + 'message' => $this->getSpotMessages(array_keys($asFeedIds['message'])), + 'media' => $this->getMedias($sMediaRefField, array_keys($asFeedIds['media'])), + 'post' => $this->getPosts(array_keys($asFeedIds['post'])) + ); + + //Replace Array Key with Item ID + foreach($asFeedAttrs as $sType=>$asFeedAttr) { + foreach($asFeedAttr as $asFeed) { + $asFeeds[$sType][$asFeed['id_'.$sType]] = $asFeed; + } + } + + //Assign + foreach($asItems as &$asItem) { + $asItem = array_merge($asFeeds[$asItem['type']][$asItem['id']], $asItem); + } + + return array('ref_id_last'=>$iRefIdLast, 'ref_id_first'=>$iRefIdFirst, 'sort'=>$sSort, 'feed'=>$asItems); } public function syncMedias() { diff --git a/index.php b/index.php index 1f3f54f..36d979f 100755 --- a/index.php +++ b/index.php @@ -24,7 +24,6 @@ $sAction = isset($_REQUEST['a'])?$_REQUEST['a']:''; $sTimezone = $_REQUEST['t'] ?? ''; $sName = isset($_GET['name'])?$_GET['name']:''; $sContent = isset($_GET['content'])?$_GET['content']:''; -$iChunk = isset($_GET['chunk'])?$_GET['chunk']:0; $iProjectId = $_REQUEST['id_project'] ?? 0; $sField = isset($_REQUEST['field'])?$_REQUEST['field']:''; $oValue = isset($_REQUEST['value'])?$_REQUEST['value']:''; @@ -44,8 +43,11 @@ if($sAction!='') case 'markers': $sResult = $oSpot->getMarkers(); break; - case 'feed': - $sResult = $oSpot->getNewsFeed($iChunk, $iId); + case 'next_feed': + $sResult = $oSpot->getNextFeed($iId); + break; + case 'new_feed': + $sResult = $oSpot->getNewFeed($iId); break; case 'add_post': $sResult = $oSpot->addPost($sName, $sContent); diff --git a/masks/project.html b/masks/project.html index 078cf5f..d1ec455 100644 --- a/masks/project.html +++ b/masks/project.html @@ -1,4 +1,5 @@