Add kanjivg data
Build and test / build (push) Successful in 10m0s

This commit is contained in:
2026-03-03 13:47:59 +09:00
parent 8ba7c66e67
commit bbdb177fa4
20 changed files with 1195 additions and 1 deletions
+175
View File
@@ -0,0 +1,175 @@
import 'package:jadb/_data_ingestion/sql_writable.dart';
/// Enum set in the kvg:position attribute, used by `<g>` elements in the KanjiVG SVG files.
enum KanjiPathGroupPosition {
upperA,
upperB,
lower1,
lower2,
bottom,
kamae,
kamaec,
left,
middle,
nyo,
nyoc,
right,
tare,
tarec,
top;
static KanjiPathGroupPosition? fromString(String? str) {
if (str == null) return null;
switch (str) {
case '⿵A':
return KanjiPathGroupPosition.upperA;
case '⿵B':
return KanjiPathGroupPosition.upperB;
case '⿶1':
return KanjiPathGroupPosition.lower1;
case '⿶2':
return KanjiPathGroupPosition.lower2;
case 'bottom':
return KanjiPathGroupPosition.bottom;
case 'kamae':
return KanjiPathGroupPosition.kamae;
case 'kamaec':
return KanjiPathGroupPosition.kamaec;
case 'left':
return KanjiPathGroupPosition.left;
case 'middle':
return KanjiPathGroupPosition.middle;
case 'nyo':
return KanjiPathGroupPosition.nyo;
case 'nyoc':
return KanjiPathGroupPosition.nyoc;
case 'right':
return KanjiPathGroupPosition.right;
case 'tare':
return KanjiPathGroupPosition.tare;
case 'tarec':
return KanjiPathGroupPosition.tarec;
case 'top':
return KanjiPathGroupPosition.top;
default:
throw ArgumentError('Unknown position: $str');
}
}
}
enum KanjiVGRadical {
general,
jis,
nelson,
tradit;
static KanjiVGRadical? fromString(String? str) {
if (str == null) return null;
switch (str) {
case 'general':
return KanjiVGRadical.general;
case 'jis':
return KanjiVGRadical.jis;
case 'nelson':
return KanjiVGRadical.nelson;
case 'tradit':
return KanjiVGRadical.tradit;
default:
throw ArgumentError('Unknown radical: $str');
}
}
}
/// Contents of a \<g> element in the KanjiVG SVG files.
class KanjiPathGroupTreeNode extends SQLWritable {
final int id;
final List<KanjiPathGroupTreeNode> children;
final String? element;
final String? original;
final KanjiPathGroupPosition? position;
final KanjiVGRadical? radical;
final int? part;
// Currently unused data.
final bool radicalForm;
final bool tradForm;
final bool partial;
final String? variant;
KanjiPathGroupTreeNode({
required this.id,
this.children = const [],
this.element,
this.original,
this.position,
this.radical,
this.part,
this.variant,
this.radicalForm = false,
this.tradForm = false,
this.partial = false,
});
@override
Map<String, Object?> get sqlValue => {
'groupId': id,
'element': element,
'original': original,
'position': position?.name,
'radical': radical?.name,
'part': part,
};
}
/// Contents of a `<text>` element in the StrokeNumber's group in the KanjiVG SVG files
class KanjiStrokeNumber extends SQLWritable {
final int num;
final double x;
final double y;
KanjiStrokeNumber(this.num, this.x, this.y);
@override
Map<String, Object?> get sqlValue => {'strokeNum': num, 'x': x, 'y': y};
}
/// Contents of a `<path>` element in the KanjiVG SVG files
class KanjiVGPath extends SQLWritable {
final int id;
final int groupId;
final String? type;
final String svgPath;
KanjiVGPath({
required this.id,
required this.groupId,
required this.type,
required this.svgPath,
});
@override
Map<String, Object?> get sqlValue => {
'pathId': id,
'groupId': groupId,
'type': type,
'svgPath': svgPath,
};
}
class KanjiVGItem extends SQLWritable {
final String character;
final List<KanjiVGPath> paths;
final List<KanjiStrokeNumber> strokeNumbers;
final List<KanjiPathGroupTreeNode> pathGroups;
KanjiVGItem({
required this.character,
required this.paths,
required this.strokeNumbers,
required this.pathGroups,
});
@override
Map<String, Object?> get sqlValue => {'character': character};
}
+112
View File
@@ -0,0 +1,112 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:jadb/_data_ingestion/kanjivg/objects.dart';
import 'package:xml/xml.dart';
List<KanjiVGItem> parseKanjiVGData(Directory rootDir) {
final List<KanjiVGItem> items = [];
for (final file in rootDir.listSync()) {
if (file is File && file.path.endsWith('.svg')) {
final String rawSVG = file.readAsStringSync();
final XmlDocument doc = XmlDocument.parse(rawSVG);
final strokePathsGroup = doc
.findAllElements('g')
.firstWhereOrNull(
(e) => e.getAttribute('id')?.startsWith('kvg:StrokePaths') ?? false,
);
final strokeNumbersGroup = doc
.findAllElements('g')
.firstWhereOrNull(
(e) =>
e.getAttribute('id')?.startsWith('kvg:StrokeNumbers') ?? false,
);
final pathGroups = strokePathsGroup != null
? _parsePathGroups(strokePathsGroup)
: <KanjiPathGroupTreeNode>[];
final strokeNumbers = strokeNumbersGroup != null
? _parseStrokeNumbers(strokeNumbersGroup)
: <KanjiStrokeNumber>[];
final paths = strokePathsGroup != null
? _parsePaths(strokePathsGroup)
: <KanjiVGPath>[];
items.add(
KanjiVGItem(
character: file.uri.pathSegments.last.split('.').first,
paths: paths,
strokeNumbers: strokeNumbers,
pathGroups: pathGroups,
),
);
}
}
return items;
}
List<KanjiStrokeNumber> _parseStrokeNumbers(XmlElement group) => group
.childElements
.map((e) {
final num = int.parse(e.innerText);
final xy = e
.getAttribute('transform')!
.split('matrix(1 0 0 1 ')[1]
.split(')')[0]
.split(' ')
.map(double.parse)
.toList();
return KanjiStrokeNumber(num, xy[0], xy[1]);
})
.toList(growable: false);
List<KanjiPathGroupTreeNode> _parsePathGroups(XmlElement group) => group
.findElements('g')
.map((e) {
return KanjiPathGroupTreeNode(
// NOTE: the outermost group does not have a number
id:
int.tryParse(e.getAttribute('id')!.split('-').last.substring(1)) ??
0,
element: e.getAttribute('kvg:element'),
original: e.getAttribute('kvg:original'),
variant: e.getAttribute('kvg:variant'),
position: KanjiPathGroupPosition.fromString(
e.getAttribute('kvg:position'),
),
radical: KanjiVGRadical.fromString(e.getAttribute('kvg:radical')),
part: int.tryParse(e.getAttribute('kvg:part') ?? ''),
radicalForm: e.getAttribute('kvg:radicalForm') == 'true',
tradForm: e.getAttribute('kvg:tradForm') == 'true',
partial: e.getAttribute('kvg:partial') == 'true',
children: _parsePathGroups(e),
);
})
.toList(growable: false);
List<KanjiVGPath> _parsePaths(XmlElement group) => group
.findAllElements('g')
.map(
(g) => g
.findElements('path')
.map(
(e) => KanjiVGPath(
id: int.parse(e.getAttribute('id')!.split('-').last.substring(1)),
groupId:
int.tryParse(
g.getAttribute('id')!.split('-').last.substring(1),
) ??
0,
type: e.getAttribute('kvg:type'),
svgPath: e.getAttribute('d')!,
),
),
)
.expand((x) => x)
.toList(growable: false);
@@ -0,0 +1,53 @@
import 'package:jadb/_data_ingestion/kanjivg/objects.dart';
import 'package:jadb/table_names/kanjivg.dart';
import 'package:sqflite_common/sqflite.dart';
Future<void> seedKanjiVGData(Iterable<KanjiVGItem> items, Database db) {
return db.transaction((txn) async {
await txn.execute('PRAGMA defer_foreign_keys = ON');
final b = txn.batch();
for (final item in items) {
b.insert(KanjiVGTableNames.entry, item.sqlValue);
for (final path in item.paths) {
b.insert(
KanjiVGTableNames.path,
path.sqlValue..addAll({'character': item.character}),
);
}
for (final strokeNumber in item.strokeNumbers) {
b.insert(
KanjiVGTableNames.strokeNumber,
strokeNumber.sqlValue..addAll({'character': item.character}),
);
}
for (final pathGroup in item.pathGroups) {
_insertPathGroup(b, null, pathGroup, item.character);
}
}
await b.commit(noResult: true);
});
}
/// Recursively insert path groups and their children
void _insertPathGroup(
Batch b,
int? parentGroupId,
KanjiPathGroupTreeNode node,
String character,
) {
b.insert(
KanjiVGTableNames.pathGroup,
node.sqlValue
..addAll({'character': character, 'parentGroupId': parentGroupId}),
);
for (final child in node.children) {
_insertPathGroup(b, node.id, child, character);
}
}
+16
View File
@@ -4,6 +4,8 @@ import 'package:jadb/_data_ingestion/jmdict/seed_data.dart';
import 'package:jadb/_data_ingestion/jmdict/xml_parser.dart';
import 'package:jadb/_data_ingestion/kanjidic/seed_data.dart';
import 'package:jadb/_data_ingestion/kanjidic/xml_parser.dart';
import 'package:jadb/_data_ingestion/kanjivg/parser.dart';
import 'package:jadb/_data_ingestion/kanjivg/seed_data.dart';
import 'package:jadb/_data_ingestion/radkfile/parser.dart';
import 'package:jadb/_data_ingestion/radkfile/seed_data.dart';
import 'package:jadb/_data_ingestion/tanos-jlpt/csv_parser.dart';
@@ -17,6 +19,7 @@ Future<void> seedData(Database db) async {
await parseAndSeedDataFromRADKFILE(db);
await parseAndSeedDataFromKANJIDIC(db);
await parseAndSeedDataFromTanosJLPT(db);
await parseAndSeedDataFromKanjiVG(db);
print('Performing VACUUM');
await db.execute('VACUUM');
@@ -102,3 +105,16 @@ Future<void> parseAndSeedDataFromTanosJLPT(Database db) async {
print('[TANOS-JLPT] Writing to database...');
await seedTanosJLPTData(resolvedEntries, db);
}
Future<void> parseAndSeedDataFromKanjiVG(Database db) async {
final kanjivgPath = Platform.environment['KANJIVG_PATH'] ?? 'data/kanjivg';
if (!Directory(kanjivgPath).existsSync()) {
throw Exception('KANJIVG directory not found at $kanjivgPath');
}
print('[KANJIVG] Parsing content...');
final items = parseKanjiVGData(Directory(kanjivgPath));
print('[KANJIVG] Writing to database...');
await seedKanjiVGData(items, db);
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:equatable/equatable.dart';
import 'kanjivg_path.dart';
import 'kanjivg_path_group.dart';
/// A full KanjiVG entry for a single character.
class KanjiVGEntry extends Equatable {
/// The kanji or character this entry belongs to.
final String character;
/// All stroke paths in drawing order.
///
/// Each path includes the rendered position of its stroke label.
final List<KanjiVGPath> paths;
/// The hierarchical group structure of the entry.
///
/// These are not really used in mugiten at the moment, so querying them is optional.
final List<KanjiVGPathGroup>? pathGroups;
KanjiVGEntry({
required this.character,
this.paths = const [],
this.pathGroups = const [],
}) : assert(
paths.isEmpty ||
(paths.first.pathId == 1 &&
paths.last.pathId == paths.length &&
paths.every((p) => p.pathId > 0)),
'Paths must be listed in a strictly growing order without holes, starting from pathId 1.',
);
@override
List<Object?> get props => [character, paths, pathGroups];
Map<String, dynamic> toJson() => {
'character': character,
'paths': paths.map((e) => e.toJson()).toList(),
'pathGroups': pathGroups?.map((e) => e.toJson()).toList(),
};
factory KanjiVGEntry.fromJson(Map<String, dynamic> json) => KanjiVGEntry(
character: json['character'] as String,
paths: ((json['paths'] as List<dynamic>?) ?? const [])
.map((e) => KanjiVGPath.fromJson(Map<String, dynamic>.from(e as Map)))
.toList(),
pathGroups: ((json['pathGroups'] as List<dynamic>?))
?.map(
(e) => KanjiVGPathGroup.fromJson(Map<String, dynamic>.from(e as Map)),
)
.toList(),
);
}
+55
View File
@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
/// A stroke path from a KanjiVG entry.
class KanjiVGPath extends Equatable {
/// The path id within the KanjiVG entry.
final int pathId;
/// The optional KanjiVG stroke type.
final String? type;
/// The raw SVG `d` path string.
final String svgPath;
/// The x-coordinate of the rendered stroke-label position.
final double labelX;
/// The y-coordinate of the rendered stroke-label position.
final double labelY;
KanjiVGPath({
required this.pathId,
required this.type,
required this.svgPath,
required this.labelX,
required this.labelY,
}) : assert(pathId > 0, 'pathId must be a positive integer. Found $pathId.'),
assert(svgPath.isNotEmpty, 'svgPath cannot be empty.'),
assert(
labelX.isFinite,
'labelX must be a finite number. Found $labelX.',
),
assert(
labelY.isFinite,
'labelY must be a finite number. Found $labelY.',
);
@override
List<Object?> get props => [pathId, type, svgPath, labelX, labelY];
Map<String, dynamic> toJson() => {
'pathId': pathId,
'type': type,
'svgPath': svgPath,
'labelX': labelX,
'labelY': labelY,
};
factory KanjiVGPath.fromJson(Map<String, dynamic> json) => KanjiVGPath(
pathId: json['pathId'] as int,
type: json['type'] as String?,
svgPath: json['svgPath'] as String,
labelX: (json['labelX'] as num).toDouble(),
labelY: (json['labelY'] as num).toDouble(),
);
}
+100
View File
@@ -0,0 +1,100 @@
import 'package:equatable/equatable.dart';
import 'package:jadb/models/kanjivg/kanjivg_path.dart';
import 'kanjivg_path_group_position.dart';
import 'kanjivg_radical.dart';
/// A hierarchical path-group from a KanjiVG entry.
class KanjiVGPathGroup extends Equatable {
/// The path-group id within the entry.
final int groupId;
/// The paths directly contained in this group, in drawing order.
final List<KanjiVGPath> paths;
/// Nested child groups.
final List<KanjiVGPathGroup> children;
/// The value of the `kvg:element` attribute, if present.
final String? element;
/// The original element before simplification, if present.
final String? original;
/// Relative position of the group inside the character layout.
final KanjiVGPathGroupPosition? position;
/// Radical classification for the group.
final KanjiVGRadical? radical;
/// Part number for repeated elements, if present.
final int? part;
KanjiVGPathGroup({
required this.groupId,
this.paths = const [],
this.children = const [],
this.element,
this.original,
this.position,
this.radical,
this.part,
}) : assert(
groupId >= 0,
'groupId must be a non-negative integer. Found $groupId.',
),
assert(
paths.isEmpty ||
paths.fold<int>(
0,
(previousMax, path) => path.pathId > previousMax
? path.pathId
: throw ArgumentError(
'Paths must be listed in a strictly growing order without holes. Found pathId ${path.pathId} after $previousMax.',
),
) ==
paths.lastOrNull?.pathId,
);
@override
List<Object?> get props => [
groupId,
paths,
children,
element,
original,
position,
radical,
part,
];
Map<String, dynamic> toJson() => {
'groupId': groupId,
'paths': paths.map((e) => e.toJson()).toList(),
'children': children.map((e) => e.toJson()).toList(),
'element': element,
'original': original,
'position': position?.toJson(),
'radical': radical?.toJson(),
'part': part,
};
factory KanjiVGPathGroup.fromJson(
Map<String, dynamic> json,
) => KanjiVGPathGroup(
groupId: json['groupId'] as int,
paths: ((json['paths'] as List<dynamic>?) ?? const [])
.map((e) => KanjiVGPath.fromJson(Map<String, dynamic>.from(e as Map)))
.toList(),
children: ((json['children'] as List<dynamic>?) ?? const [])
.map(
(e) => KanjiVGPathGroup.fromJson(Map<String, dynamic>.from(e as Map)),
)
.toList(),
element: json['element'] as String?,
original: json['original'] as String?,
position: KanjiVGPathGroupPosition.fromJson(json['position']),
radical: KanjiVGRadical.fromJson(json['radical']),
part: json['part'] as int?,
);
}
@@ -0,0 +1,41 @@
/// Relative position tags used by KanjiVG path-groups.
///
/// In the original SVG files these come from the `kvg:position` attribute.
/// The database stores the normalized enum name, while [svgValue] contains the
/// raw KanjiVG attribute value.
enum KanjiVGPathGroupPosition {
upperA(svgValue: '⿵A'),
upperB(svgValue: '⿵B'),
lower1(svgValue: '⿶1'),
lower2(svgValue: '⿶2'),
bottom(svgValue: 'bottom'),
kamae(svgValue: 'kamae'),
kamaec(svgValue: 'kamaec'),
left(svgValue: 'left'),
middle(svgValue: 'middle'),
nyo(svgValue: 'nyo'),
nyoc(svgValue: 'nyoc'),
right(svgValue: 'right'),
tare(svgValue: 'tare'),
tarec(svgValue: 'tarec'),
top(svgValue: 'top');
final String svgValue;
const KanjiVGPathGroupPosition({required this.svgValue});
/// Parses either the normalized enum name stored in the database/JSON, or
/// the raw KanjiVG SVG attribute value.
static KanjiVGPathGroupPosition fromString(String value) => values.firstWhere(
(e) => e.name == value || e.svgValue == value,
orElse: () => throw Exception('Unknown position: $value'),
);
static KanjiVGPathGroupPosition? fromNullableString(String? value) =>
value == null ? null : fromString(value);
Object? toJson() => name;
static KanjiVGPathGroupPosition? fromJson(Object? json) =>
fromNullableString(json as String?);
}
+20
View File
@@ -0,0 +1,20 @@
/// Radical classification tags used by KanjiVG path-groups.
enum KanjiVGRadical {
general,
jis,
nelson,
tradit;
static KanjiVGRadical fromString(String value) => values.firstWhere(
(e) => e.name == value,
orElse: () => throw Exception('Unknown radical: $value'),
);
static KanjiVGRadical? fromNullableString(String? value) =>
value == null ? null : fromString(value);
Object? toJson() => name;
static KanjiVGRadical? fromJson(Object? json) =>
fromNullableString(json as String?);
}
+2
View File
@@ -1,5 +1,6 @@
import 'package:jadb/table_names/jmdict.dart';
import 'package:jadb/table_names/kanjidic.dart';
import 'package:jadb/table_names/kanjivg.dart';
import 'package:jadb/table_names/radkfile.dart';
import 'package:jadb/table_names/tanos_jlpt.dart';
import 'package:sqflite_common/sqlite_api.dart';
@@ -21,6 +22,7 @@ Future<void> verifyTablesWithDbConnection(DatabaseExecutor db) async {
...KANJIDICTableNames.allTables,
...RADKFILETableNames.allTables,
...TanosJLPTTableNames.allTables,
...KanjiVGTableNames.allTables,
};
final missingTables = expectedTables.difference(tables);
+12
View File
@@ -1,9 +1,11 @@
import 'package:jadb/const_data/radicals.dart';
import 'package:jadb/models/kanji_search/kanji_search_result.dart';
import 'package:jadb/models/kanjivg/kanjivg_entry.dart';
import 'package:jadb/models/verify_tables.dart';
import 'package:jadb/models/word_search/word_search_result.dart';
import 'package:jadb/search/filter_kanji.dart';
import 'package:jadb/search/kanji_search.dart';
import 'package:jadb/search/kanji_vg_search.dart';
import 'package:jadb/search/radical_search.dart';
import 'package:jadb/search/versions.dart';
import 'package:jadb/search/word_search/word_search.dart';
@@ -24,6 +26,16 @@ extension JaDBConnection on DatabaseExecutor {
Iterable<String> kanji,
) => searchManyKanjiWithDbConnection(this, kanji);
/// Search for a KanjiVG graph in the database.
Future<KanjiVGEntry?> jadbSearchKanjiVGGraph(
String kanji, {
bool includePathGroups = false,
}) => searchKanjiVGGraphWithDbConnection(
this,
kanji,
includePathGroups: includePathGroups,
);
/// Filter a list of characters, and return the ones that are listed in the kanji dictionary.
Future<List<String>> filterKanji(
Iterable<String> kanji, {
+175
View File
@@ -0,0 +1,175 @@
import 'package:jadb/models/kanjivg/kanjivg_entry.dart';
import 'package:jadb/models/kanjivg/kanjivg_path.dart';
import 'package:jadb/models/kanjivg/kanjivg_path_group.dart';
import 'package:jadb/models/kanjivg/kanjivg_path_group_position.dart';
import 'package:jadb/models/kanjivg/kanjivg_radical.dart';
import 'package:jadb/table_names/kanjivg.dart';
import 'package:sqflite_common/sqlite_api.dart';
Future<List<Map<String, Object?>>> _entryQuery(
DatabaseExecutor connection,
String entryKey,
) => connection.rawQuery(
'''
SELECT *
FROM "${KanjiVGTableNames.entry}"
WHERE "character" = ?
OR "character" LIKE ?
ORDER BY "character" != ?, "character"
LIMIT 1
''',
[entryKey, '$entryKey-%', entryKey],
);
Future<List<Map<String, Object?>>> _pathsQuery(
DatabaseExecutor connection,
String entryKey,
) => connection.rawQuery(
'''
SELECT
"${KanjiVGTableNames.path}"."pathId",
"${KanjiVGTableNames.path}"."groupId",
"${KanjiVGTableNames.path}"."type",
"${KanjiVGTableNames.path}"."svgPath",
"${KanjiVGTableNames.strokeNumber}"."x",
"${KanjiVGTableNames.strokeNumber}"."y"
FROM "${KanjiVGTableNames.path}"
JOIN "${KanjiVGTableNames.strokeNumber}"
ON "${KanjiVGTableNames.path}"."character" = "${KanjiVGTableNames.strokeNumber}"."character"
AND "${KanjiVGTableNames.path}"."pathId" = "${KanjiVGTableNames.strokeNumber}"."strokeNum"
WHERE "${KanjiVGTableNames.path}"."character" = ?
ORDER BY "${KanjiVGTableNames.path}"."pathId"
''',
[entryKey],
);
Future<List<Map<String, Object?>>> _pathGroupsQuery(
DatabaseExecutor connection,
String entryKey,
) => connection.query(
KanjiVGTableNames.pathGroup,
where: 'character = ?',
whereArgs: [entryKey],
orderBy: 'groupId',
);
String _normalizeKanjiVGEntryKey(String kanji) {
final encodedMatch = RegExp(r'^([0-9a-fA-F]{5,6})(-.+)?$').firstMatch(kanji);
if (encodedMatch != null) {
return '${encodedMatch.group(1)!.toLowerCase()}${encodedMatch.group(2) ?? ''}';
}
final runes = kanji.runes.toList(growable: false);
if (runes.length == 1) {
return runes.single.toRadixString(16).padLeft(5, '0');
}
return kanji;
}
String _characterFromEntryKey(String entryKey, String fallback) {
final encodedMatch = RegExp(r'^([0-9a-fA-F]{5,6})').firstMatch(entryKey);
if (encodedMatch == null) {
return fallback;
}
return String.fromCharCode(int.parse(encodedMatch.group(1)!, radix: 16));
}
KanjiVGPath _pathFromRow(Map<String, Object?> row) => KanjiVGPath(
pathId: row['pathId'] as int,
type: row['type'] as String?,
svgPath: row['svgPath'] as String,
labelX: (row['x'] as num).toDouble(),
labelY: (row['y'] as num).toDouble(),
);
List<KanjiVGPathGroup> _buildPathGroups(
List<Map<String, Object?>> pathRows,
List<Map<String, Object?>> pathGroupRows,
) {
final rowsByGroupId = <int, Map<String, Object?>>{
for (final row in pathGroupRows) (row['groupId'] as int?)!: row,
};
final childGroupIdsByParentGroupId = <int?, List<int>>{};
for (final row in pathGroupRows) {
final groupId = (row['groupId'] as int?)!;
final parentGroupId = row['parentGroupId'] as int?;
childGroupIdsByParentGroupId
.putIfAbsent(parentGroupId, () => [])
.add(groupId);
}
final pathsByGroupId = <int, List<KanjiVGPath>>{};
for (final row in pathRows) {
final groupId = (row['groupId'] as int?)!;
pathsByGroupId.putIfAbsent(groupId, () => []).add(_pathFromRow(row));
}
KanjiVGPathGroup buildGroup(int groupId) {
final row = rowsByGroupId[groupId]!;
return KanjiVGPathGroup(
groupId: groupId,
paths: pathsByGroupId[groupId] ?? const [],
children: (childGroupIdsByParentGroupId[groupId] ?? const [])
.map(buildGroup)
.toList(growable: false),
element: row['element'] as String?,
original: row['original'] as String?,
position: KanjiVGPathGroupPosition.fromNullableString(
row['position'] as String?,
),
radical: KanjiVGRadical.fromNullableString(row['radical'] as String?),
part: row['part'] as int?,
);
}
return (childGroupIdsByParentGroupId[null] ?? const [])
.map(buildGroup)
.toList(growable: false);
}
/// Searches for a KanjiVG graph and returns its stroke data, or null if the
/// kanji is not found in the database.
Future<KanjiVGEntry?> searchKanjiVGGraphWithDbConnection(
DatabaseExecutor connection,
String kanji, {
bool includePathGroups = false,
}) async {
final entryKey = _normalizeKanjiVGEntryKey(kanji);
final entryRows = await _entryQuery(connection, entryKey);
if (entryRows.isEmpty) {
return null;
}
final matchedEntryKey = entryRows.first['character'] as String;
late final List<Map<String, Object?>> pathRows;
List<Map<String, Object?>> pathGroupRows = const [];
if (includePathGroups) {
await Future.wait([
_pathsQuery(
connection,
matchedEntryKey,
).then((value) => pathRows = value),
_pathGroupsQuery(
connection,
matchedEntryKey,
).then((value) => pathGroupRows = value),
]);
} else {
pathRows = await _pathsQuery(connection, matchedEntryKey);
}
return KanjiVGEntry(
character: _characterFromEntryKey(matchedEntryKey, kanji),
paths: pathRows.map(_pathFromRow).toList(growable: false),
pathGroups: includePathGroups
? _buildPathGroups(pathRows, pathGroupRows)
: null,
);
}
+15
View File
@@ -0,0 +1,15 @@
abstract class KanjiVGTableNames {
static const String version = 'KanjiVG_Version';
static const String entry = 'KanjiVG_Entry';
static const String path = 'KanjiVG_Path';
static const String strokeNumber = 'KanjiVG_StrokeNumber';
static const String pathGroup = 'KanjiVG_PathGroup';
static Set<String> get allTables => {
version,
entry,
path,
strokeNumber,
pathGroup,
};
}
+1 -1
View File
@@ -1 +1 @@
const int jadbSchemaVersion = 2;
const int jadbSchemaVersion = 3;