Bookmarks: fix sort within one page#8616
Conversation
| local a_page = self.ui.document:getPageFromXPointer(a.page) | ||
| local b_page = self.ui.document:getPageFromXPointer(b.page) | ||
| if a_page == b_page then -- have bookmarks before highlights | ||
| return a.highlighted | ||
| local compare_xp = self.ui.document:compareXPointers(a.page, b.page) | ||
| if compare_xp == 0 then -- both bookmarks with the same start |
There was a problem hiding this comment.
I have a litlte concern in here, with the many calls to compareXPointers(a,b) being expensive, at least this being more expensive than before:
- getPageFromXPointer() is cached by the cre call cache (as most of the other get*() methods), while compareXPointers() is not - and if we were to cache the results of compareXPointers(a,b), we would cache N*N results if we have N highlights. The cacheing of getPageFromXPointer() for N highlights caches just N results.
- This would be ok if isBookmarkInPageOrder() was called only when adding or removing bookmark/highlights (I don't care that this spends time at the moment we do it), but it seems to be called in a few other "just reading" calls, like setDogearVisibility() (on each page, to know if we should show the dogear) or getPreviousBookmarkedPage(), and may be others.
- (same feeling may apply to the paging section above, switching out of reflow mode and doing more expensive work, not sure)
So, may be it needs a dedicated duplicate that you would call only when you need the accurate ordering - and may be keep this one as it was for the calls like setDogearVisibility that may not need the ordering accuracy ?
I also see you didn't update the next function, isBookmarkInReversePageOrder(), so there's some unbalance, that might be restored with a 3rd function that does the ordering accuracy.
There was a problem hiding this comment.
compareXPointers(a,b) being expensive
So maybe at first compare pages, and only when two bookmarks are within one page, compare xpointers.
There was a problem hiding this comment.
I also see you didn't update the next function,
isBookmarkInReversePageOrder()
This one is used only in one place for
koreader/frontend/apps/reader/modules/readerpaging.lua
Lines 318 to 320 in c11ea4f
in flipping mode, so no need of accuracy within one page.
There was a problem hiding this comment.
This one is used only in one place for
But also by this it seems:
skimtowidget.lua: self:goToByEvent("GotoNextBookmarkFromPage")
All this ReaderBookmark reorderning/insertion/binary search feels a bit crappy, thought we could get it simpler when migrating to annotations.
But my idea is that any navigation should be smooth and cheap, and only at add/remove bookmark/highlight we are allowed to do expensive stuff (expensive so it orders everything so that the others can then be smooth and cheap).
So maybe at first compare pages, and only when two bookmarks are within one page, compare xpointers.
Yes, that would be good - if you're sure about page being ok (might not be when created in reflow mode I beleive from what you wrote in your code?)
There was a problem hiding this comment.
any navigation should be smooth and cheap, and only at add/remove bookmark/highlight we are allowed to do expensive stuff
Let's decide. isBookmarkInPageOrder is used 6 times:
fixBookmarkSort
addBookmark
isBookmarkAdded
removeBookmark
getDogearBookmarkIndex
getPreviousBookmarkedPage
The first group needs accuracy. You propose to make my isBookmarkInPositionOrder for it only, right?
There was a problem hiding this comment.
The first group needs accuracy. You propose to make my isBookmarkInPositionOrder for it only, right?
Yes, exactly, that's the idea: a new function, that you would use where you needed to have stuff fixed.
- isBookmarkInPageOrder / isBookmarkInReversePageOrder for quick navigation cheap stuff where only page is enough (mainly, comparing page in binary search)
- isBookmarkInPositionOrder for fixBookmarkSort and addBookmark (which can do reordering/insertion).
- isBookmarkAdded / removeBookmark don't do any insert/ordering, so it feels they could use the cheaper one (isBookmarkAdded is used only by DictQuickLookup).
When you first talked about this PR some days ago, I though you might need a one step migration: fixBookmarkSort() did a migration (and may do it for books not opened since long), so you might want to either make it better and have it be called again (if config:hasNot("bookmarks_sorted2")), or have another fixBookmarkSort2() so your isBookmarkInPositionOrder() can work on something already perfectly sorted (or may be it doesn't need it ? and will do the sorting? but then, not until another bookmark is added?).
There was a problem hiding this comment.
Okay. What about removeBookmark? It makes the full scan when there are several bookmarks in a page.
Using isBookmarkInPositionOrder we can remove full scan code, it is not used (I've checked).
koreader/frontend/apps/reader/modules/readerbookmark.lua
Lines 903 to 906 in ec280f8
EDIT: no, cannot remove full scan code, but it would be used much rarely.
There was a problem hiding this comment.
That means you would need to have your bookmarks correctly sorted with your new way, after an initial migration ?
But for removeBookmark(), I wonder if doing binary search and calling your (expensive) compareXPointers() might be slower than a full scan :) (but may be that's no more an issue as you first check the are identical before using it).
(I think it's me that added the fallback full scan, may be because I was in a situation that shouldn't happen once you have precisely sorted the bookmark ? Dunno.)
|
Fixed an extremely edge case existing crash. |
|
3 calls of quick isBookmarkInPageOrder, 3 calls of thorough isBookmarkInPositionOrder. |
| local box1 = self:getWordFromPosition(doc, ppos1).pbox | ||
| local box2 = self:getWordFromPosition(doc, ppos2).pbox | ||
| if box1.y == box2.y then |
There was a problem hiding this comment.
Dunno what ppos1 looks like, it doesn't have any y ? Or does it have a y but it may not be "current" ?
There was a problem hiding this comment.
comparePositions function is used in ReaderBookmark and in ReaderHighlight (extended selection).
In ReaderHighlight one of the positions is the hold_pos, so at first we need to get a word under the long-press and then compare.
EDIT: I've refreshed memory of my development of extended selection: pos is not a wordbox upper left corner, but just a position of long-press-start or long-press-release. So we always need to get words under both positions at first.
There was a problem hiding this comment.
OK, so you would need this only when you invoke comparePositions() from extendSelection() ?
When you invoke comparePositions() from isBookmarkInPositionOrder(), you know you have bookmarks that are not random screen coordinates, but are proper word boxes ?
So, may be it could have another parameter to say we need to go thru this or not?
There was a problem hiding this comment.
Sorry, missed your comment.
pos0 and pos1 stored in pdf bookmarks are not box positions, but touch positions.
We always need to get a word and its box's x and y.
There was a problem hiding this comment.
OK, well, then OK.
(I suppose the correct word/box are available in the bookmark counterpart in the "highlight" list, which has "pboxes" = [ {x/y/w/h}, {x/y/w/h}... ] ? Not asking you to go fetch them here, just wondering if these are correct, and it this would be solved if we would move the pboxes from highlight into bookmarks if one day, we get rid of "highlight".)
There was a problem hiding this comment.
That's a good idea, pboxes are ordered and it is easy to get coordinates from them and compare.
We already have pboxes in some bookmarks (when adding a note, to save it in the pdf file).
In the meantime, currently pboxes are stored in highlights, but used only when saving highlight to pdf. If this option is disabled, pboxes are not needed.
Readerview does not use them to draw the highlights, just positions
koreader/frontend/apps/reader/modules/readerview.lua
Lines 484 to 485 in 7163a39
| if config:hasNot("bookmarks_sorted") then | ||
| table.sort(self.bookmarks, function(a, b) | ||
| return self:isBookmarkInPageOrder(a, b) | ||
| return self:isBookmarkInPositionOrder(a, b) |
There was a problem hiding this comment.
Don't you think you need to have this re-done once for past books, so you're sure all bookmarks are precisely sorted?
If not, and if we have past highlights in a same page in that order (let's say these are y) [10, 30, 20, 40], I'm not sure where your addBookmark may insert a new highlight at y=25: before the 30 or after the 20 ? and the order will stay being not ordered.
if config:hasNot("bookmarks_sorted_20220106") then ?
There was a problem hiding this comment.
I propose to add a button Sort bookmarks to the extra button menu, to allow a user to sort if he needs (maybe he doesn't want).
And to remove bookmarks_sorted from doc_settings.
There was a problem hiding this comment.
I propose to add a button Sort bookmarks to the extra button menu, to allow a user to sort if he needs (maybe he doesn't want).
Mhhh, no, this doesn't feel it should be a user choice. It's a technical internal ordering of the data structure, for efficiency purpose, to have an accurate binary search.
If "he doesn't want", I guess he wants to see bookmarks ordered by date added - but then it would still be by page, and only bookmarks inside a same page would be by "date added". So, a user facing button should be: Sort by page (and position inside a same page) / Sort by date added (with some new work for you) - but I think sort by date added is a bit useless :)
And to remove bookmarks_sorted from doc_settings.
And to what aim ?:
- still calling fixBookmarkSort and have table.sort(...isBookmarkInPositionOrder) done on each book opening ? (feels like an expensive no-op after you have done it once, and you have addBoookmark always insert correctly.)
- or not doing any sorting fix at all ?
There was a problem hiding this comment.
- or not doing any sorting fix at all
I thought of that, and a button for manual sorting. OK, dropped.
There was a problem hiding this comment.
config:hasNot("bookmarks_sorted_20220106")
Yes, I like the idea, re-sort all.
|
What do you think about highlighting images? koreader/frontend/apps/reader/modules/readerhighlight.lua Lines 1588 to 1605 in ec280f8 It is connected with #579 and koreader/plugins/exporter.koplugin/clip.lua Lines 267 to 279 in ec280f8 but currently we cannot do this. Is it needed? Maybe, remove all this old stuff? |
Not much, I didn't even know that was possible with PDF :) @mustafa-001 seemed to care about clipping images in #5794. |
|
I'm going to fix Exporter plugin sorting order. Maybe remove this export of "highlighted image" that doesn't work at all? And this commented code from readerhighlight? |
|
Dunno, I'm usually for not touching stuff that does not originate from me :) |
|
I enabled one-time accurate sorting for the test book (von Mises, 282 bms), it took about 10 sec on my Kindle to re-sort them, all perfectly. |
| self.ui.doc_settings:makeTrue("bookmarks_sorted") | ||
| self.ui.doc_settings:delSetting("bookmarks_sorted") | ||
| self.ui.doc_settings:makeTrue("bookmarks_sorted_20220106") |
There was a problem hiding this comment.
I would let self.ui.doc_settings:makeTrue("bookmarks_sorted") and don't mind it.
We don't really need to have docsettings that tidy :)
But more importantly, think about people trying an older KOReader version (for comparing behaviour, regressions...):
- they have a document with
bookmarks_sorted, upgrade, getbookmarks_sorted_20220106, and getbookmarks_sortedremoved - they switch back to v2021.10: the old fixBookmark is run (because
bookmarks_sortedis not there) which might re-order your nice ordering.
(I don't mind much how bookmark behave on v2021.10, we should just try to be sure it doesn't crash) - they switch back to v2022.02: you don't run your nice re-ordering because
bookmarks_sorted_20220106is there, but you have badly re-ordered bookmarks as done by v2021.10 (and you get reports for bugs and issues you have a hard time understanding :)
|
Unrelated, just mentionning this in the latest bookmark PR: [3] = {
["notes"] = "ces meubles",
["pos0"] = "/body/DocFragment[9]/body/p[135]/text().2586",
["pos1"] = "/body/DocFragment[9]/body/p[135]/text().2597",
["highlighted"] = true,
["page"] = "/body/DocFragment[9]/body/p[135]/text().2586",
["chapter"] = "5",
["datetime"] = "2022-01-09 02:04:51",
},and no equivalent in |
|
Cannot imagine. Datetime is saved once when creating a highlight/bookmark and is never changed. which then calls getHighlightBookmarkItem, and in it:This cannot give 3 sec difference (and time request for hl is first, earlier by microseconds). |
Sounds like an oversight and a bug - and may explain why it's rare and me doing this highlight at maybe 02:04:51.999 was bad luck :)
Yes, but may be 3 minutes differences I observed was because there may have been a 1 second difference, and then when trying to play around, I deleted the highlight (which because of the 1 second difference, did not get the bookmark deleted), then remade it 3 mn later - and I may have ending up with 2 bookmarks and 1 highlight for the same thing, one bookmark among the 2 having no highlight buddy. |
Maybe the reason is koreader/frontend/ui/widget/dictquicklookup.lua Lines 917 to 920 in a1cbc3b |
|
(I don't remember having used the dictquicklookup "Highlight" button, if I get that this is used by it - actually, I hack this button out in my own koreader patch :) so I'm sure I didn't hit it.) |
|
If by "the reason" you mean why one has to compute a datetime because it could be used standalone, when not used standalone, a datetime could be passed around so it is used instead, and computed only when nothing is passed. (seems kludgy, but well, that's our datastructure :) |
|
Got a crash when opening some old test document, so possibly edited and having bookmark/xpointers no longer found in the document (I think I mentionned that case in another issue): logging what we got: local compare_xp = self.ui.document:compareXPointers(a.page, b.page)
+ logger.warn(a.page, b.page, compare_xp)And indeed, in the C function, we may return nil when a xpointer is not found: static int compareXPointers(lua_State *L){
CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument");
const char* xp1 = luaL_checkstring(L, 2);
const char* xp2 = luaL_checkstring(L, 3);
ldomXPointerEx nodep1 = doc->dom_doc->createXPointer(lString32(xp1));
ldomXPointerEx nodep2 = doc->dom_doc->createXPointer(lString32(xp2));
if (nodep1.isNull() || nodep2.isNull())
return 0; -- returning nil when xpointer not found
// Return 1 if pointers are ordered (if xp2 is after xp1), -1 if not, 0 if same
lua_pushinteger(L, nodep2.compare(nodep1));
return 1;
}Dunno how we'd like to handle this case, sorting wise - but I think we should not crash, and if possible, not remove the wrong bookmark (as one might edit and restore the document to the previous good state, and would like to find again his bookmarks). |
|
Can we check if xpointer is correct? koreader/frontend/apps/reader/modules/readerbookmark.lua Lines 231 to 234 in 87dbb98 and if not, compare pages only? |
|
We can: if a_page == b_page then -- both bookmarks in the same page
local compare_xp = self.ui.document:compareXPointers(a.page, b.page)
+ if compare_xp == nil then -- some xpointer not found (?)
+ logger.warn(a.page, b.page, a_page, b_page)
+ logger.warn(a.page, self.ui.document:isXPointerInDocument(a.page))
+ logger.warn(b.page, self.ui.document:isXPointerInDocument(b.page))
+ end
if compare_xp == 0 then -- both bookmarks with the same startBut I'd rather do it later than line 231, only in the rare case we find out compare_xp didn't work and that one (or both) of the compared xpointer is invalid. Note that for invalid xpointers, So, given that these invalid xpointers (that most people will not have) get to be announced as being on page 1, may be sort them before all other valid ones (those really on page 1, but most of them on pages 1++). |
|
So it can be local a_page = self.ui.document:getPageFromXPointer(a.page)
local b_page = self.ui.document:getPageFromXPointer(b.page)
if a_page == b_page then -- both bookmarks in the same page
local compare_xp = self.ui.document:compareXPointers(a.page, b.page)
if compare_xp then
if compare_xp == 0 then -- both bookmarks with the same start
if a.highlighted and b.highlighted then -- both are highlights, compare ends
return self.ui.document:compareXPointers(a.pos1, b.pos1) < 0
end
return a.highlighted -- have page bookmarks before highlights
end
return compare_xp < 0
end
end
return a_page > b_page
Invalid bookmarks will be sorted to the page 1. After correcting the book, a user will need to resort bookmarks to restore their correct positions, so need to delete |
|
Oh, that simple :) Good!
Well, I guess we don't need to really bother with that. Again, it's rare, and people like me messing with content get what they deserve :) (except a crash was more than what they deserved :) Unless you feel it's quickly doable and doesn't hurt (a logger.info() telling an invalid was found will help noticing if this happens when it should not). |
|
|
OK, so not much issue with doing nothing ? :) |
Accurate sorting of bookmarks located in one page depending on their positions in text.
Bookmarks of one page will be sorted depending on their position in the text (now they are sorted in order of adding).
Page bookmark is the first (before highlights and notes).
If two highlights start at one position, the shorter one will be the first.
This change is