New algorithm for first-letter partitioning

This commit is contained in:
Charles Haley 2011-02-25 13:23:32 +00:00
parent f907da1433
commit c703ade9ff

View File

@ -9,7 +9,7 @@ Browsing book collection by tags.
import traceback, copy, cPickle import traceback, copy, cPickle
from itertools import izip from itertools import izip, repeat
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
@ -534,7 +534,7 @@ class TagTreeItem(object): # {{{
ROOT = 2 ROOT = 2
def __init__(self, data=None, category_icon=None, icon_map=None, def __init__(self, data=None, category_icon=None, icon_map=None,
parent=None, tooltip=None, category_key=None): parent=None, tooltip=None, category_key=None, temporary=False):
self.parent = parent self.parent = parent
self.children = [] self.children = []
self.id_set = set() self.id_set = set()
@ -552,6 +552,7 @@ class TagTreeItem(object): # {{{
self.bold_font.setBold(True) self.bold_font.setBold(True)
self.bold_font = QVariant(self.bold_font) self.bold_font = QVariant(self.bold_font)
self.category_key = category_key self.category_key = category_key
self.temporary = temporary
elif self.type == self.TAG: elif self.type == self.TAG:
icon_map[0] = data.icon icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
@ -1086,17 +1087,17 @@ class TagsModel(QAbstractItemModel): # {{{
else: else:
collapse_model = 'partition' collapse_model = 'partition'
collapse_template = tweaks['categories_collapsed_popularity_template'] collapse_template = tweaks['categories_collapsed_popularity_template']
collapse_letter = collapse_letter_sk = None
def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): def process_one_node(category, state_map):
collapse_letter = None
category_index = self.createIndex(category.row(), 0, category) category_index = self.createIndex(category.row(), 0, category)
category_node = category_index.internalPointer() category_node = category_index.internalPointer()
key = category_node.category_key key = category_node.category_key
if key not in data: if key not in data:
return ((collapse_letter, collapse_letter_sk)) return
cat_len = len(data[key]) cat_len = len(data[key])
if cat_len <= 0: if cat_len <= 0:
return ((collapse_letter, collapse_letter_sk)) return
category_child_map = {} category_child_map = {}
fm = self.db.field_metadata[key] fm = self.db.field_metadata[key]
@ -1105,6 +1106,41 @@ class TagsModel(QAbstractItemModel): # {{{
not fm['kind'] == 'user' \ not fm['kind'] == 'user' \
else False else False
tt = key if fm['kind'] == 'user' else None tt = key if fm['kind'] == 'user' else None
if collapse_model == 'first letter':
# Build a list of 'equal' first letters by looking for
# overlapping ranges. If a range overlaps another, then the
# letters are assumed to be equivalent. ICU collating is complex
# beyond belief. This mechanism lets us determine the logical
# first character from ICU's standpoint.
chardict = {}
for idx,tag in enumerate(data[key]):
if not tag.sort:
c = ' '
else:
c = icu_upper(tag.sort[0])
if c not in chardict:
chardict[c] = [idx, idx]
else:
chardict[c][1] = idx
# sort the ranges to facilitate detecting overlap
ranges = sorted([(v[0], v[1], c) for c,v in chardict.items()])
# Create a list of 'first letters' to use for each item in
# the category. The list is generated using the ranges. Overlaps
# are filled with the character that first occurs.
cl_list = list(repeat(None, len(data[key])))
for t in ranges:
start = t[0]
c = t[2]
if cl_list[start] is None:
nc = c
else:
nc = cl_list[start]
for i in range(start, t[1]+1):
cl_list[i] = nc
for idx,tag in enumerate(data[key]): for idx,tag in enumerate(data[key]):
if clear_rating: if clear_rating:
tag.avg_rating = None tag.avg_rating = None
@ -1121,30 +1157,19 @@ class TagsModel(QAbstractItemModel): # {{{
name = eval_formatter.safe_format(collapse_template, name = eval_formatter.safe_format(collapse_template,
d, 'TAG_VIEW', None) d, 'TAG_VIEW', None)
self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1) self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1)
sub_cat = TagTreeItem(parent=category, sub_cat = TagTreeItem(parent=category, data = name,
data = name, tooltip = None, tooltip = None, temporary=True,
category_icon = category_node.icon, category_icon = category_node.icon,
category_key=category_node.category_key) category_key=category_node.category_key)
self.endInsertRows() self.endInsertRows()
else: else:
ts = tag.sort cl = cl_list[idx]
if not ts: if cl != collapse_letter:
ts = ' ' collapse_letter = cl
try:
sk = sort_key(ts)[0]
except:
sk = ts[0]
if sk != collapse_letter_sk:
collapse_letter = upper(ts[0])
try:
collapse_letter_sk = sort_key(collapse_letter)[0]
except:
collapse_letter_sk = collapse_letter
sub_cat = TagTreeItem(parent=category, sub_cat = TagTreeItem(parent=category,
data = collapse_letter, data = collapse_letter,
category_icon = category_node.icon, category_icon = category_node.icon,
tooltip = None, tooltip = None, temporary=True,
category_key=category_node.category_key) category_key=category_node.category_key)
node_parent = sub_cat node_parent = sub_cat
else: else:
@ -1200,7 +1225,7 @@ class TagsModel(QAbstractItemModel): # {{{
# This id_set must not be None # This id_set must not be None
node_parent.id_set |= tag.id_set node_parent.id_set |= tag.id_set
return ((collapse_letter, collapse_letter_sk)) return
for category in self.category_nodes: for category in self.category_nodes:
if len(category.children) > 0: if len(category.children) > 0:
@ -1208,7 +1233,11 @@ class TagsModel(QAbstractItemModel): # {{{
states = [c.tag.state for c in category.child_tags()] states = [c.tag.state for c in category.child_tags()]
names = [(c.tag.name, c.tag.category) for c in category.child_tags()] names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
state_map = dict(izip(names, states)) state_map = dict(izip(names, states))
ctags = [c for c in child_map if c.type == TagTreeItem.CATEGORY] # temporary sub-categories (the partitioning ones) must follow
# the permanent sub-categories. This will happen naturally if
# the temp ones are added by process_node
ctags = [c for c in child_map if
c.type == TagTreeItem.CATEGORY and not c.temporary]
start = len(ctags) start = len(ctags)
self.beginRemoveRows(self.createIndex(category.row(), 0, category), self.beginRemoveRows(self.createIndex(category.row(), 0, category),
start, len(child_map)-1) start, len(child_map)-1)
@ -1217,8 +1246,7 @@ class TagsModel(QAbstractItemModel): # {{{
else: else:
state_map = {} state_map = {}
collapse_letter, collapse_letter_sk = process_one_node(category, process_one_node(category, state_map)
state_map, collapse_letter, collapse_letter_sk)
return True return True
def columnCount(self, parent): def columnCount(self, parent):