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/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,
);
}