@@ -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};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
@@ -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?);
|
||||
}
|
||||
@@ -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?);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -1 +1 @@
|
||||
const int jadbSchemaVersion = 2;
|
||||
const int jadbSchemaVersion = 3;
|
||||
|
||||
Reference in New Issue
Block a user