PageRenderTime 60ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/legacy/ui/tags.js

https://github.com/flynx/ImageGrid
JavaScript | 584 lines | 359 code | 108 blank | 117 comment | 106 complexity | 293a8b1fe4d2fda2a7a2386228d23ca4 MD5 | raw file
  1. /**********************************************************************
  2. *
  3. *
  4. *
  5. **********************************************************************/
  6. // NOTE: if this is set to null the feature will be disabled...
  7. var UNSORTED_TAG = 'unsorted'
  8. // Tag index
  9. //
  10. // This can be constructed from tags in IMAGES with buildTagsFromImages(..)
  11. //
  12. // format:
  13. // {
  14. // tag: [ gid, ... ],
  15. // ...
  16. // }
  17. //
  18. // NOTE: unlike MARKED and BOOKMARKS the gid lists here are not sparse.
  19. // XXX do we need to make this sparse??
  20. var TAGS = {}
  21. /*********************************************************************/
  22. function buildTagsFromImages(images){
  23. images = images == null ? IMAGES : images
  24. var tagset = {}
  25. var order = DATA.order
  26. for(var gid in images){
  27. var tags = images[gid].tags
  28. // no tags in this image...
  29. if(tags == null || tags.length == 0){
  30. continue
  31. }
  32. tags.forEach(function(tag){
  33. // first time we see this tag...
  34. if(tagset[tag] == null){
  35. tagset[tag] = []
  36. }
  37. // only update if not tagged...
  38. if(tagset[tag].indexOf(gid) < 0){
  39. // NOTE: this is cheating, but it's ~5x faster than
  40. // insertGIDToPosition(..) but still 10^2 slower
  41. // than .push(..) (unsorted tags)
  42. tagset[tag][order.indexOf(gid)] = gid
  43. }
  44. })
  45. }
  46. // cleanup...
  47. for(var tag in tagset){
  48. tagset[tag] = tagset[tag].filter(function(e){ return e != null })
  49. }
  50. TAGS = tagset
  51. tagsUpdated()
  52. return tagset
  53. }
  54. // XXX think I need to do something a-la fickr-style normalization here...
  55. // XXX also need to remember the original notation...
  56. function normalizeTag(tag){
  57. return tag.trim()
  58. }
  59. /**********************************************************************
  60. * Actions...
  61. */
  62. function getTags(gid){
  63. // XXX should we do any more checking here?
  64. return IMAGES[gid].tags
  65. }
  66. function addTag(tags, gid, tagset, images){
  67. tags = typeof(tags) == typeof('str') ? [ tags ] : tags
  68. gid = gid == null ? getImageGID() : gid
  69. tagset = tagset == null ? TAGS : tagset
  70. images = images == null ? IMAGES : images
  71. if(tags.length == 0){
  72. return
  73. }
  74. var updated = false
  75. var img = images[gid]
  76. img_tags = img.tags == null ? [] : img.tags
  77. // add tags to tagset...
  78. tags.map(function(tag){
  79. // skip empty tags...
  80. if(normalizeTag(tag) == ''){
  81. return
  82. }
  83. updated = true
  84. var set = tagset[tag]
  85. if(set == null){
  86. set = []
  87. tagset[tag] = set
  88. }
  89. if(set.indexOf(gid) < 0){
  90. insertGIDToPosition(gid, set)
  91. }
  92. if(img_tags.indexOf(tag) < 0){
  93. img_tags.push(tag)
  94. }
  95. })
  96. if(updated){
  97. img.tags = img_tags
  98. imageUpdated(gid)
  99. tagsUpdated()
  100. }
  101. }
  102. function removeTag(tags, gid, tagset, images){
  103. tags = typeof(tags) == typeof('str') ? [ tags ] : tags
  104. gid = gid == null ? getImageGID() : gid
  105. tagset = tagset == null ? TAGS : tagset
  106. images = images == null ? IMAGES : images
  107. if(tags.length == 0){
  108. return
  109. }
  110. var updated = false
  111. var img = images[gid]
  112. tags.map(function(tag){
  113. var set = tagset[tag]
  114. // remove tags from tagset...
  115. if(set != null && set.indexOf(gid) >= 0){
  116. updated = true
  117. set.splice(set.indexOf(tag), 1)
  118. }
  119. // remove tags from image...
  120. if(img.tags != null && img.tags.indexOf(tag) >= 0){
  121. updated = true
  122. img.tags.splice(img.tags.indexOf(tag), 1)
  123. }
  124. })
  125. // clear the tags...
  126. if(updated && img.tags != null && img.tags.length == 0){
  127. delete img.tags
  128. }
  129. if(updated){
  130. imageUpdated(gid)
  131. tagsUpdated()
  132. }
  133. }
  134. // Update the tags of an image to the given list of tags...
  135. //
  136. // The resulting tag set of the image will exactly match the given list.
  137. //
  138. // NOTE: this will potentially both add and remove tags...
  139. function updateTags(tags, gid, tagset, images){
  140. tags = typeof(tags) == typeof('str') ? [ tags ] : tags
  141. gid = gid == null ? getImageGID() : gid
  142. tagset = tagset == null ? TAGS : tagset
  143. images = images == null ? IMAGES : images
  144. var img = images[gid]
  145. // remove...
  146. if(img.tags != null){
  147. var rtags = []
  148. img.tags.map(function(tag){
  149. if(tags.indexOf(tag) < 0){
  150. rtags.push(tag)
  151. }
  152. })
  153. removeTag(rtags, gid, tagset, images)
  154. }
  155. // add...
  156. addTag(tags, gid, tagset, images)
  157. }
  158. /**********************************************************************
  159. * Selectors...
  160. */
  161. // Select gids tagged by ALL given tags...
  162. //
  163. function tagSelectAND(tags, from, no_sort, tagset){
  164. tags = typeof(tags) == typeof('str') ? [ tags ] : tags
  165. tagset = tagset == null ? TAGS : tagset
  166. from = from == null ? getLoadedGIDs() : getLoadedGIDs(from)
  167. // special case: a single tag...
  168. // NOTE: this is significantly faster.
  169. if(tags.length == 1){
  170. var res = tagset[tags[0]]
  171. return res == null
  172. ? []
  173. : res.filter(function(gid){
  174. // skip unloaded...
  175. return from.indexOf(gid) >= 0
  176. })
  177. }
  178. var res = []
  179. var subtagset = []
  180. // populate the subtagset...
  181. tags.map(function(tag){
  182. if(tagset[tag] == null){
  183. subtagset = false
  184. return false
  185. }
  186. subtagset.push(tagset[tag])
  187. })
  188. // if at least one tag is invalid, we'll find nothing...
  189. if(subtagset == false){
  190. return []
  191. }
  192. // sort index by length...
  193. subtagset.sort(function(a, b){
  194. return b.length - a.length
  195. })
  196. // start with the shortest subset...
  197. var cur = subtagset.pop().slice()
  198. // filter out the result...
  199. cur.map(function(gid){
  200. for(var i=0; i < subtagset.length; i++){
  201. if(subtagset[i].indexOf(gid) < 0){
  202. gid = null
  203. break
  204. }
  205. }
  206. // populate res...
  207. if(gid != null && from.indexOf(gid) >= 0){
  208. //no_sort == true ? res.push(gid) : insertGIDToPosition(gid, res)
  209. res.push(gid)
  210. }
  211. })
  212. if(!no_sort){
  213. fastSortGIDsByOrder(res)
  214. }
  215. return res
  216. }
  217. // select gids untagged by ALL of the given tags...
  218. //
  219. function tagSelectNOT(tags, from, tagset){
  220. var remove = tagSelectAND(tags, from, false, tagset)
  221. // keep the elements that DO NOT exist in remove...
  222. return from.filter(function(e){
  223. return remove.indexOf(e) < 0
  224. })
  225. }
  226. // Select gids tagged by ANY of the given tags...
  227. //
  228. function tagSelectOR(tags, from, no_sort, tagset){
  229. tags = typeof(tags) == typeof('str') ? [ tags ] : tags
  230. tagset = tagset == null ? TAGS : tagset
  231. from = from == null ? getLoadedGIDs() : from
  232. var all = []
  233. tags.forEach(function(tag){
  234. tag = tagset[tag]
  235. tag = tag == null ? [] : tag
  236. all = all.concat(tag)
  237. })
  238. return from.filter(function(e){
  239. return all.indexOf(e) >= 0
  240. })
  241. }
  242. /**********************************************************************
  243. * List oriented tag operations...
  244. */
  245. function tagList(list, tags){
  246. list = getLoadedGIDs(list)
  247. list
  248. .forEach(function(gid){
  249. addTag(tags, gid)
  250. })
  251. return list
  252. }
  253. function untagList(list, tags){
  254. list = getLoadedGIDs(list)
  255. list
  256. .forEach(function(gid){
  257. removeTag(tags, gid)
  258. })
  259. return list
  260. }
  261. // same as tagList(..), but will also remove the tags form gids no in
  262. // list...
  263. function tagOnlyList(list, tags, no_sort){
  264. no_sort = no_sort == null ? true : false
  265. tagSelectAND(tags, null, no_sort)
  266. .forEach(function(gid){
  267. if(list.indexOf(gid) < 0){
  268. removeTag(tags, gid)
  269. }
  270. })
  271. return tagList(list, tags)
  272. }
  273. // tag manipulation of ribbon images...
  274. function tagRibbon(tags){ return tagList(getRibbonGIDs(), tags) }
  275. function untagRibbon(tags){ return untagList(getRibbonGIDs(), tags) }
  276. function tagOnlyRibbon(tags){ return tagOnlyList(getRibbonGIDs(), tags) }
  277. // tag manipulation of marked images...
  278. function tagMarked(tags){ return tagList(MARKED, tags) }
  279. function untagMarked(tags){ return untagList(MARKED, tags) }
  280. function tagOnlyMarked(tags){ return tagOnlyList(MARKED, tags) }
  281. // tag manipulation of bookmarked images...
  282. function tagBookmarked(tags){ return tagList(BOOKMARKS, tags) }
  283. function untagBookmarked(tags){ return untagList(BOOKMARKS, tags) }
  284. function tagOnlyBookmarked(tags){ return tagOnlyList(BOOKMARKS, tags) }
  285. /*********************************************************************/
  286. // marking of tagged images...
  287. function markTagged(tags){
  288. MARKED = tagSelectAND(tags)
  289. updateImages()
  290. marksUpdated()
  291. return MARKED
  292. }
  293. function unmarkTagged(tags){
  294. var set = tagSelectAND(tags, null, false)
  295. set.forEach(function(gid){
  296. var i = MARKED.indexOf(gid)
  297. if(i > -1){
  298. MARKED.splice(i, 1)
  299. }
  300. })
  301. updateImages()
  302. marksUpdated()
  303. return set
  304. }
  305. /*********************************************************************/
  306. // Tag image that are not neighbored by an image tagged with the same
  307. // tags from at least one side.
  308. //
  309. // Essentially this will list tag block borders.
  310. //
  311. // NOTE: this will consider each gids's ribbon context rather than the
  312. // straight order context...
  313. // XXX this is slow...
  314. function listTagsAtGapsFrom(tags, gids){
  315. gids = gids == null ? getLoadedGIDs() : gids
  316. var list = tagSelectAND(tags, gids)
  317. var res = []
  318. list.forEach(function(gid){
  319. var ribbon = DATA.ribbons[getGIDRibbonIndex(gid)]
  320. var i = ribbon.indexOf(gid)
  321. // add the current gid to the result iff one or both gids
  322. // adjacent to it are not in the list...
  323. if(list.indexOf(ribbon[i-1]) < 0
  324. || list.indexOf(ribbon[i+1]) < 0){
  325. res.push(gid)
  326. }
  327. })
  328. return res
  329. }
  330. function getGapEdge(direction, tag, gids){
  331. var get = direction == 'next' ? getGIDAfter : getGIDBefore
  332. gids = gids == null ? getRibbonGIDs() : gids
  333. var tagged = TAGS[tag].filter(function(e){ return gids.indexOf(e) >= 0 })
  334. var cur = getImageGID()
  335. var step = direction == 'next' ? 1 : -1
  336. // get the next element...
  337. cur = gids[gids.indexOf(cur) + step]
  338. // current is tagged -- skip till first untagged (not inclusive)...
  339. if(getTags(cur) != null && getTags(cur).indexOf(tag) >= 0){
  340. // skip the current...
  341. var i = gids.indexOf(cur)
  342. // get last in tag block...
  343. while(i >= 0 && i < gids.length
  344. // lookahead -- we...
  345. && tagged.indexOf(gids[i+step]) >= 0){
  346. i += step
  347. }
  348. var res = gids[i]
  349. // current is not tagged -- get closest tagged (inclusive)...
  350. } else {
  351. var res = get(cur, tagged)
  352. }
  353. if(res == null){
  354. flashIndicator(direction == 'next' ? 'end' : 'start')
  355. return getImage()
  356. }
  357. return showImage(res)
  358. }
  359. function nextGapEdge(tag, gids){
  360. return getGapEdge('next', tag, gids)
  361. }
  362. function prevGapEdge(tag, gids){
  363. return getGapEdge('prev', tag, gids)
  364. }
  365. function nextUnsortedSection(gids){
  366. return getGapEdge('next', 'unsorted', gids)
  367. }
  368. function prevUnsortedSection(gids){
  369. return getGapEdge('prev', 'unsorted', gids)
  370. }
  371. /*********************************************************************/
  372. // cropping of tagged images...
  373. function cropTagged(tags, keep_ribbons, keep_unloaded_gids){
  374. var set = tagSelectAND(tags)
  375. cropDataTo(set, keep_ribbons, keep_unloaded_gids)
  376. return DATA
  377. }
  378. /**********************************************************************
  379. * Files...
  380. */
  381. var loadFileTags = makeFileLoader(
  382. 'Tags',
  383. CONFIG.tags_file,
  384. false,
  385. function(data){
  386. // no tags loaded -- rebuild...
  387. if(data === false){
  388. var t0 = Date.now()
  389. buildTagsFromImages()
  390. var t1 = Date.now()
  391. console.warn('Tags: build tags.json: done ('+( t1 - t0 )+'ms) -- re-save the data.')
  392. // load the tags...
  393. } else {
  394. TAGS = data
  395. }
  396. })
  397. // Save image marks to file
  398. var saveFileTags = makeFileSaver(
  399. 'Tags',
  400. CONFIG.tags_file,
  401. function(){
  402. return TAGS
  403. })
  404. function tagsUpdated(){
  405. fileUpdated('Tags')
  406. $('.viewer').trigger('tagsUpdated')
  407. }
  408. /**********************************************************************
  409. * Dialogs/panels...
  410. */
  411. // XXX add tags to:
  412. // - loaded images
  413. // - marked images
  414. function batchTagImagesDialog(){
  415. // XXX
  416. }
  417. /**********************************************************************
  418. * Setup...
  419. */
  420. // Setup the unsorted image state managers...
  421. //
  422. // Unsorted tags are removed by shifting or by aligning, but only in
  423. // uncropped mode...
  424. function setupUnsortedTagHandler(viewer){
  425. console.log('Tags: "'+UNSORTED_TAG+'" tag handling: setup...')
  426. return viewer
  427. // unsorted tag handling...
  428. .on('shiftedImage', function(evt, img){
  429. if(UNSORTED_TAG != null && !isViewCropped()){
  430. removeTag(UNSORTED_TAG, getImageGID(img))
  431. }
  432. })
  433. .on('shiftedImages', function(evt, gids){
  434. if(UNSORTED_TAG != null && !isViewCropped()){
  435. untagList(gids, UNSORTED_TAG)
  436. }
  437. })
  438. .on('aligningRibbonsSection', function(evt, base, gids){
  439. if(UNSORTED_TAG != null && !isViewCropped()){
  440. untagList(gids, UNSORTED_TAG)
  441. }
  442. })
  443. .on('sortedImages', function(){
  444. for(var tag in TAGS){
  445. TAGS[tag] = fastSortGIDsByOrder(TAGS[tag])
  446. }
  447. tagsUpdated()
  448. })
  449. .on('horizontalShiftedImage', function(evt, gid, direction){
  450. var updated = false
  451. for(var tag in TAGS){
  452. if(TAGS[tag].indexOf(gid) >= 0){
  453. updated = shiftGIDToOrderInList(gid, direction, TAGS[tag])
  454. }
  455. }
  456. if(updated){
  457. tagsUpdated()
  458. }
  459. })
  460. // when the gid is swapped update the cache...
  461. .on('updatedImageGID', function(evt, was, is){
  462. var updated = false
  463. for(var tag in TAGS){
  464. var i = TAGS[tag].indexOf(was)
  465. if(i >= 0){
  466. TAGS[tag][i] = is
  467. }
  468. }
  469. if(updated){
  470. tagsUpdated()
  471. }
  472. })
  473. }
  474. SETUP_BINDINGS.push(setupUnsortedTagHandler)
  475. /**********************************************************************
  476. * vim:set ts=4 sw=4 : */