Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Client-side paging #878

Draft
wants to merge 76 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
a8a6b81
Add explanation about `addHighlightField` method
Jul 27, 2022
3043018
Add Basic JSON Representation for Document and First Navigation Button
Aug 22, 2022
fc96716
Refactor: Format code
Aug 22, 2022
12e2fa2
Refactor: Move JS from template to new Navigation.js
Aug 22, 2022
60c8230
Make loaded document a global variable
Aug 22, 2022
949f4dd
Fix naming pageBack vs. pageStepBack
Aug 22, 2022
a68f39d
Handle all forward/back navigation buttons
Aug 22, 2022
9c46d23
Enable/disable nav buttons on client, + refactor
Aug 22, 2022
bf15ca1
Create WIP documentation page
Aug 23, 2022
90e65c5
Include page object in pageChanged event, start typing
Aug 23, 2022
3c2499d
Update metadata table on page change
Aug 23, 2022
3d2bd9d
Add Data Structure for Client Side Fulltext Reload
Aug 23, 2022
a79caa2
Adapt Fulltext Datastructure to Metadata and Document
Aug 23, 2022
6891a3a
Update navigation select
beatrycze-volk Aug 23, 2022
bda7d4a
Update links for navigation buttons
beatrycze-volk Aug 23, 2022
f1e59d7
Fix reference to fulltextControl
Aug 23, 2022
1ebf970
Set navigation select value on page change
Aug 23, 2022
0cd5cf3
Fix: First load data in `Doc::getLogicalSectionsOnPage`
Aug 23, 2022
f6f9d97
Start implementing history/browser navigation
Aug 23, 2022
69398a3
Refactor: In document JSON, store pages in subkey
Aug 23, 2022
1e3261a
Refactor: Wrap image URL & MIME type into subkey
Aug 23, 2022
128cb35
Extract Doc::getPageLink()
Aug 24, 2022
6a4044d
List download URLs in document JSON
Aug 24, 2022
9b862d2
Update page download links on page change
Aug 24, 2022
10b1841
Refactor: Avoid manual JSON encoding
Aug 24, 2022
944c654
Include all file groups/URLs in document JSON
Aug 24, 2022
0f2b668
Update image download links on page change
Aug 24, 2022
1209a75
Start implementing client-side double page mode
Aug 24, 2022
a678322
Simplify: Remove pageObj and target from event detail
Aug 24, 2022
fe4a9cd
Simplify: Merge `pageChanged` and `configChanged` events
Aug 24, 2022
d03ac0a
Fix back buttons when using route enhancers
Aug 24, 2022
b755540
Generalize URL generation to work with slugs
Aug 24, 2022
61d7d31
Fix pdf and image download links
Aug 25, 2022
ade3dae
TOC: Add option to render full table of contents
Jul 5, 2022
0826f5b
Add client side navigation from table of contents
Aug 25, 2022
9c284c4
Add separate marker for metadata list
Aug 25, 2022
850c6db
Start implementing dynamic TOC expansion
Aug 25, 2022
53f7a52
Update URL on page change
Aug 25, 2022
f8379ba
Optimize document JSON generation for METS
Aug 25, 2022
059d278
Fix disabling of lastPage navigation button
Aug 25, 2022
7658762
Cache pageview layers
Aug 25, 2022
432072d
Update Page URL Template for Use with Page Grid
Aug 26, 2022
38972d5
Add document plugin for JSON representation
Aug 26, 2022
c37df7c
Move document initialisation to document plugin
Aug 26, 2022
195a39c
Move document handling to dlfController via event
Aug 26, 2022
2f274ba
Start moving getVisiblePages to dlfController
Aug 26, 2022
e0244c9
Use `getVisiblePages()` of `dlfController`
Aug 27, 2022
a8d2c99
Refactor: Extract `dlfController::eventTarget`
Aug 27, 2022
5556f0a
Refactor: Don't dispatch `stateChanged` manually
Aug 27, 2022
16a101a
Refactor: Dissolve global `tx_dlf_loaded`
Aug 27, 2022
80611c3
[EXT] Reinstate registry
Aug 27, 2022
f3dd140
[EXT] Memoize `MetsDocument::getStructureDepth()`
Aug 27, 2022
e48d5c4
Fetch prerendererd metadata dynamically
Aug 27, 2022
f32e3cf
Convert `TODO` to `TODO(client-side)`
Aug 27, 2022
c8be739
Update and extend documentation
Aug 29, 2022
dce4b78
Update .editorconfig (4 spaces for JS) to reflect actual use
Aug 29, 2022
fe0010b
Extend documentation
Aug 30, 2022
5ae4604
Add and update notes about URL generation
Aug 30, 2022
56e67fd
Fix Codacy warnings
beatrycze-volk Jan 20, 2023
6080908
Fix toArray method in Doc class
beatrycze-volk Feb 3, 2023
2b96402
Adjust DocumentController
beatrycze-volk Feb 6, 2023
7c06baa
Move nonProxyMimeTypes to config
beatrycze-volk Feb 6, 2023
95d5893
Implement getAllFiles for IIIFManifest
beatrycze-volk Feb 28, 2023
8d4040c
Remove unused manifest variable
beatrycze-volk Feb 28, 2023
463d6f8
Fix intend in css
beatrycze-volk Mar 8, 2023
196511b
Improve Metadata script
beatrycze-volk Mar 8, 2023
6df21dc
Improve Navigation script
beatrycze-volk Mar 8, 2023
0cc2e40
Assign fulltextEntry directly
beatrycze-volk Mar 28, 2023
228606e
Fix comment
beatrycze-volk Mar 28, 2023
85c001b
Allow console logs
beatrycze-volk Mar 29, 2023
45d6642
Activate full text highlight if full text is active
beatrycze-volk Mar 30, 2023
2b46f50
Fix errors marked by PHPStan
beatrycze-volk Oct 25, 2023
c6acd25
Fix Codacy errors
beatrycze-volk Oct 25, 2023
009ddb5
Fix error: Direct use of $GLOBALS Superglobal detected
beatrycze-volk Nov 20, 2023
73fd42a
Fix documentation build
beatrycze-volk Aug 2, 2024
b4c4773
Fix PHPStan warnings
beatrycze-volk Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ trim_trailing_whitespace = true

# TS/JS-Files
[*.{ts,js}]
indent_size = 2
indent_size = 4

# JSON-Files
[*.json]
Expand Down
2 changes: 2 additions & 0 deletions .github/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ parameters:
- '#Call to an undefined method Kitodo\\Dlf\\Domain\\Repository\\[a-zA-Z]+Repository::findOneByUid\(\)\.#'
- '#Call to an undefined method Psr\\Http\\Message\\RequestFactoryInterface::request\(\)\.#'
- '#Call to an undefined method Solarium\\Core\\Query\\DocumentInterface::setField\(\)\.#'
- '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\CanvasInterface::getImages\(\)\.#'
- '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\CanvasInterface::getOtherContent\(\)\.#'
- '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\IiifResourceInterface::getHeight\(\)\.#'
- '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\IiifResourceInterface::getWidth\(\)\.#'
- '#Call to an undefined method Ubl\\Iiif\\Presentation\\Common\\Model\\Resources\\IiifResourceInterface::getPossibleTextAnnotationContainers\(\)\.#'
Expand Down
2 changes: 1 addition & 1 deletion Classes/Command/BaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ protected function getParentDocumentUidForSaving(Document $document, bool $softC

if ($doc !== null && !empty($doc->parentHref)) {
// find document object by record_id of parent
$parent = AbstractDocument::getInstance($doc->parentHref, ['storagePid' => $this->storagePid]);
$parent = AbstractDocument::getInstance($doc->parentHref, 0, ['storagePid' => $this->storagePid]);

if ($parent->recordId) {
$parentDocument = $this->documentRepository->findOneByRecordId($parent->recordId);
Expand Down
2 changes: 1 addition & 1 deletion Classes/Command/DeleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ private function getDocument($input): ?Document
if (MathUtility::canBeInterpretedAsInteger($input->getOption('doc'))) {
$document = $this->documentRepository->findByUid($input->getOption('doc'));
} elseif (GeneralUtility::isValidUrl($input->getOption('doc'))) {
$doc = AbstractDocument::getInstance($input->getOption('doc'), ['storagePid' => $this->storagePid], true);
$doc = AbstractDocument::getInstance($input->getOption('doc'), 0, ['storagePid' => $this->storagePid], true);

if ($doc->recordId) {
$document = $this->documentRepository->findOneByRecordId($doc->recordId);
Expand Down
2 changes: 1 addition & 1 deletion Classes/Command/HarvestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$docLocation = $baseLocation . http_build_query($params);
// ...index the document...
$document = null;
$doc = AbstractDocument::getInstance($docLocation, ['storagePid' => $this->storagePid], true);
$doc = AbstractDocument::getInstance($docLocation, 0, ['storagePid' => $this->storagePid], true);

if ($doc === null) {
$io->warning('WARNING: Document "' . $docLocation . '" could not be loaded. Skip to next document.');
Expand Down
4 changes: 2 additions & 2 deletions Classes/Command/IndexCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$io->error('ERROR: Document with UID "' . $input->getOption('doc') . '" could not be found on PID ' . $this->storagePid . ' .');
return BaseCommand::FAILURE;
} else {
$doc = AbstractDocument::getInstance($document->getLocation(), ['storagePid' => $this->storagePid], true);
$doc = AbstractDocument::getInstance($document->getLocation(), 0, ['storagePid' => $this->storagePid], true);
}

} else if (GeneralUtility::isValidUrl($input->getOption('doc'))) {
$doc = AbstractDocument::getInstance($input->getOption('doc'), ['storagePid' => $this->storagePid], true);
$doc = AbstractDocument::getInstance($input->getOption('doc'), 0, ['storagePid' => $this->storagePid], true);

$document = $this->getDocumentFromUrl($doc, $input->getOption('doc'));
}
Expand Down
2 changes: 1 addition & 1 deletion Classes/Command/ReindexCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

foreach ($documents as $id => $document) {
$doc = AbstractDocument::getInstance($document->getLocation(), ['storagePid' => $this->storagePid], true);
$doc = AbstractDocument::getInstance($document->getLocation(), 0, ['storagePid' => $this->storagePid], true);

if ($doc === null) {
$io->warning('WARNING: Document "' . $document->getLocation() . '" could not be loaded. Skip to next document.');
Expand Down
152 changes: 146 additions & 6 deletions Classes/Common/AbstractDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ abstract class AbstractDocument
*/
protected int $cPid = 0;

/**
* @access protected
* @var int This holds the page ID for the requests
*/
protected int $pageId = 0;

/**
* @access public
* @static
Expand Down Expand Up @@ -515,6 +521,27 @@ abstract protected function prepareMetadataArray(int $cPid): void;
*/
abstract protected function setPreloadedDocument($preloadedDocument): bool;

/**
* Get information about all files contained in the document, or null if this information is not available.
*
* Returns an associative array of the following form:
*
* ```php
* [
* '#FILE_ID' => [
* 'url' => '...',
* 'mimetype' => '...',
* ],
* // ...
* ]
* ```
*
* @access public
*
* @return array
*/
abstract public function getAllFiles(): array;

/**
* This is a singleton class, thus an instance must be created by this method
*
Expand All @@ -523,12 +550,13 @@ abstract protected function setPreloadedDocument($preloadedDocument): bool;
* @static
*
* @param string $location The URL of XML file or the IRI of the IIIF resource
* @param int $pageId
* @param array $settings
* @param bool $forceReload Force reloading the document instead of returning the cached instance
*
* @return AbstractDocument|null Instance of this class, either MetsDocument or IiifManifest
*/
public static function &getInstance(string $location, array $settings = [], bool $forceReload = false)
public static function &getInstance(string $location, int $pageId = 0, array $settings = [], bool $forceReload = false)
{
// Create new instance depending on format (METS or IIIF) ...
$documentFormat = null;
Expand Down Expand Up @@ -576,14 +604,14 @@ public static function &getInstance(string $location, array $settings = [], bool
// Sanitize input.
$pid = array_key_exists('storagePid', $settings) ? max((int) $settings['storagePid'], 0) : 0;
if ($documentFormat == 'METS') {
$instance = new MetsDocument($pid, $location, $xml, $settings);
$instance = new MetsDocument($pid, $location, $pageId, $xml, $settings);
} elseif ($documentFormat == 'IIIF') {
// TODO: Parameter $preloadedDocument of class Kitodo\Dlf\Common\IiifManifest constructor expects SimpleXMLElement|Ubl\Iiif\Presentation\Common\Model\Resources\IiifResourceInterface, Ubl\Iiif\Presentation\Common\Model\AbstractIiifEntity|null given.
// @phpstan-ignore-next-line
$instance = new IiifManifest($pid, $location, $iiif);
$instance = new IiifManifest($pid, $location, $pageId, $iiif);
}

if ($instance !== null) {
if ($instance) {
self::setDocumentCache($location, $instance);
}

Expand Down Expand Up @@ -1140,7 +1168,7 @@ protected function magicGetRootId(): int
if ($this->parentId) {
// TODO: Parameter $location of static method AbstractDocument::getInstance() expects string, int<min, -1>|int<1, max> given.
// @phpstan-ignore-next-line
$parent = self::getInstance($this->parentId, ['storagePid' => $this->pid]);
$parent = self::getInstance($this->parentId, $this->pageId, ['storagePid' => $this->pid]);
$this->rootId = $parent->rootId;
}
$this->rootIdLoaded = true;
Expand Down Expand Up @@ -1188,14 +1216,16 @@ protected function _setCPid(int $value): void
*
* @param int $pid If > 0, then only document with this PID gets loaded
* @param string $location The location URL of the XML file to parse
* @param int $pageId
* @param \SimpleXMLElement|IiifResourceInterface $preloadedDocument Either null or the \SimpleXMLElement
* or IiifResourceInterface that has been loaded to determine the basic document format.
*
* @return void
*/
protected function __construct(int $pid, string $location, $preloadedDocument, array $settings = [])
protected function __construct(int $pid, string $location, int $pageId, $preloadedDocument, array $settings = [])
{
$this->pid = $pid;
$this->pageId = $pageId;
$this->setPreloadedDocument($preloadedDocument);
$this->init($location, $settings);
$this->establishRecordId($pid);
Expand Down Expand Up @@ -1301,4 +1331,114 @@ private static function setDocumentCache(string $location, AbstractDocument $cur
// Save value in cache
$cache->set($cacheIdentifier, $currentDocument);
}

/**
* Get IDs of logical structures that a page belongs to, indexed by depth.
*
* @param int $pageNo
* @return array
*/
public function getLogicalSectionsOnPage($pageNo)
{
$this->magicGetSmLinks();
$this->magicGetPhysicalStructure();

$ids = [];
if (!empty($this->physicalStructure[$pageNo]) && !empty($this->smLinks['p2l'][$this->physicalStructure[$pageNo]])) {
foreach ($this->smLinks['p2l'][$this->physicalStructure[$pageNo]] as $logId) {
$depth = $this->getStructureDepth($logId);
$ids[$depth][] = $logId;
}
}
ksort($ids);
reset($ids);
return $ids;
}

/**
* Get URL of download file of specified page, or the empty string if there is no such link.
*
* @param int $pageNumber
* @return string
*/
public function getPageLink($pageNumber)
{
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
$fileGrpsDownload = GeneralUtility::trimExplode(',', $extConf['files']['fileGrpDownload']);
// Get image link.
foreach ($fileGrpsDownload as $fileGrpDownload) {
if (!empty($this->physicalStructureInfo[$this->physicalStructure[$pageNumber]]['files'][$fileGrpDownload])) {
return $this->getFileLocation($this->physicalStructureInfo[$this->physicalStructure[$pageNumber]]['files'][$fileGrpDownload]);
}
}
return '';
}

public function toArray($uriBuilder, array $config = [])
{
$this->magicGetSmLinks();
$this->magicGetPhysicalStructure();

$proxyFileGroups = $config['proxyFileGroups'] ?? [];
$forceAbsoluteUrl = $config['forceAbsoluteUrl'] ?? false;
$minPage = $config['minPage'] ?? 1;
$maxPage = $config['maxPage'] ?? $this->numPages;

$result = [
'pages' => [],
'query' => [
'minPage' => $minPage
]
];

$allFiles = $this->getAllFiles();

for ($page = $minPage; $page <= $maxPage; $page++) {
$pageEntry = [
'logSections' => array_merge(...$this->getLogicalSectionsOnPage($page)),
'files' => [],
];

foreach ($this->physicalStructureInfo[$this->physicalStructure[$page]]['files'] as $fileGrp => $fileId) {
if (!$allFiles) {
$file = [
'url' => $this->getFileLocation($fileId),
'mimetype' => $this->getFileMimeType($fileId),
];
} else {
$file = $allFiles[$fileId] ?? null;
if ($file === null) {
continue;
}
}

$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
$nonProxyMimeTypes = GeneralUtility::trimExplode(',', $extConf['nonProxyMimeTypes']);

// Only deliver static images via the internal PageViewProxy.
// (For IIP and IIIF, the viewer needs to build and access a separate metadata URL, see `getMetadataURL`.)
if (in_array($fileGrp, $proxyFileGroups) && !in_array($file['mimetype'], $nonProxyMimeTypes)) {
// Configure @action URL for form.
$file['url'] = $uriBuilder
->reset()
->setTargetPageUid($this->pageId)
->setCreateAbsoluteUri($forceAbsoluteUrl)
->setArguments(
[
'eID' => 'tx_dlf_pageview_proxy',
'url' => $file['url'],
'uHash' => GeneralUtility::hmac($file['url'], 'PageViewProxy')
]
)
->build();
}

$pageEntry['files'][$fileGrp] = $file;
}

$result['pages'][] = $pageEntry;
}

return $result;
}
}
53 changes: 51 additions & 2 deletions Classes/Common/IiifManifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,56 @@ public function getFileMimeType(string $id): string
return $format;
}

/**
* @see AbstractDocument::getAllFiles()
*/
public function getAllFiles(): array
{
$files = [];
$canvases = $this->iiif->getDefaultCanvases();
foreach ($canvases as $canvas) {
$images = $canvas->getImages();
foreach ($images as $image) {
$resource = $image->getResource();
$fileId = $resource->getId();
if (empty($fileId)) {
continue;
}

$mimetype = $this->getFileMimeType($fileId);
if (empty($mimetype)) {
continue;
}

$files[$fileId] = [
'url' => $fileId,
'mimetype' => $mimetype,
];
}

$otherFiles = $canvas->getOtherContent();
foreach ($otherFiles as $otherFile) {
$fileId = $$otherFile->getId();
if (empty($fileId)) {
continue;
}

$mimetype = $this->getFileMimeType($fileId);
if (empty($mimetype)) {
continue;
}

// in IIIF id is URL
$files[$fileId] = [
'url' => $fileId,
'mimetype' => $mimetype,
];
}

}
return $files;
}

/**
* @see AbstractDocument::getLogicalStructure()
*/
Expand Down Expand Up @@ -854,8 +904,7 @@ protected function ensureHasFulltextIsSet(): void
* https://digi.ub.uni-heidelberg.de/diglit/iiif/hirsch_hamburg1933_04_25/list/0001.json
*/
if (!$this->hasFulltextSet && $this->iiif instanceof ManifestInterface) {
$manifest = $this->iiif;
$canvases = $manifest->getDefaultCanvases();
$canvases = $this->iiif->getDefaultCanvases();
foreach ($canvases as $canvas) {
if (
!empty($canvas->getSeeAlsoUrlsForFormat("application/alto+xml")) ||
Expand Down
2 changes: 1 addition & 1 deletion Classes/Common/Indexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public static function add(Document $document, DocumentRepository $documentRepos
$parent = $documentRepository->findByUid($parentId);
if ($parent) {
// get XML document of parent
$doc = AbstractDocument::getInstance($parent->getLocation(), ['storagePid' => $parent->getPid()], true);
$doc = AbstractDocument::getInstance($parent->getLocation(), 0, ['storagePid' => $parent->getPid()], true);
if ($doc !== null) {
$parent->setCurrentDocument($doc);
$success = self::add($parent, $documentRepository);
Expand Down
Loading