units
This commit is contained in:
parent
bdabc7928b
commit
29208f8c40
9 changed files with 410 additions and 41 deletions
55
README.md
55
README.md
|
|
@ -31,10 +31,65 @@ The file is a dart array of formulas. Each formula is a dart set literal
|
|||
},
|
||||
"d4rt_code": "F = m*a;"
|
||||
|
||||
},
|
||||
{
|
||||
"name": 'Test argument order',
|
||||
"input": [
|
||||
'z':{ "magnitude": 'scalar'},
|
||||
'a':{ "magnitude": 'scalar'},
|
||||
'y':{ "magnitude": 'scalar'},
|
||||
],
|
||||
"output": { 'result', "magnitude": 'scalar' },
|
||||
"d4rtCode": '''
|
||||
result = a * 100 + y * 10 + z;
|
||||
''',
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# Unit file description
|
||||
|
||||
```dart
|
||||
[
|
||||
{
|
||||
"name": 'meter',
|
||||
"symbol": 'm',
|
||||
"isBase": true
|
||||
},
|
||||
{
|
||||
"name": 'inch',
|
||||
"symbol" 'in',
|
||||
"baseUnit": 'meter',
|
||||
"factor": 0.0254
|
||||
},
|
||||
{
|
||||
"name": 'nautical mile',
|
||||
"symbol": 'Nm',
|
||||
"baseUnit": 'meter',
|
||||
"factor": 1852
|
||||
},
|
||||
{
|
||||
"name": 'Kelvin',
|
||||
"symbol": "Kº",
|
||||
"isBase": true,
|
||||
},
|
||||
{
|
||||
"name": 'Celsius',
|
||||
"symbol": "Cº",
|
||||
"baseUnit" : "Kelvin",
|
||||
"toBase": "x + 273.15",
|
||||
"fromBase": "x - 273.15"
|
||||
},
|
||||
{
|
||||
"name": 'Fahrenheit',
|
||||
"symbol": "Fº",
|
||||
"baseUnit" : "Kelvin",
|
||||
"toBase": "(x - 32) × 5/9 + 273.15",
|
||||
"fromBase": "x - 273.15) * 9/5 + 32"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Formula Search and Computation
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# launch as: firejail --profile=firejail-warp-terminal.profile /opt/warpdotdev/warp-terminal/warp
|
||||
private /home/alvaro/repos/d4rt-formulas/
|
||||
private /home/alvaro/repos/d4rt_formulas/
|
||||
blacklist /datos-1T
|
||||
blacklist /datos-luks/
|
||||
blacklist /media
|
||||
|
|
@ -8,3 +8,4 @@ blacklist /media
|
|||
# noroot # don't run as root
|
||||
caps.drop all # drop Linux capabilities
|
||||
# seccomp # enable syscall filtering
|
||||
First version of a formula widget
|
||||
96
lib/corpus.dart
Normal file
96
lib/corpus.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import 'package:d4rt/d4rt.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
||||
|
||||
class Multimap<K, V> extends DelegatingMap<K, List<V>> {
|
||||
final Map<K, List<V>> _map;
|
||||
|
||||
Multimap(super.map) : _map = map;
|
||||
|
||||
factory Multimap.create() {
|
||||
return Multimap({});
|
||||
}
|
||||
|
||||
@override
|
||||
List<V>? operator [](Object? key) {
|
||||
if (_map.containsKey(key)) {
|
||||
return super[key];
|
||||
}
|
||||
final List<V> newList = [];
|
||||
super[key as K] = newList;
|
||||
return super[key];
|
||||
}
|
||||
}
|
||||
|
||||
class UnitCorpus {
|
||||
final Multimap<String, String> _baseToUnits = Multimap.create();
|
||||
final Map<String, UnitSpec> _allUnits = {};
|
||||
|
||||
void loadUnits(List<UnitSpec> units, [bool replaceOnDuplicates = false]) {
|
||||
for (final unit in units) {
|
||||
if (!replaceOnDuplicates && _allUnits.containsKey(unit.name)) {
|
||||
throw ArgumentError("Duplicate unit:${unit}");
|
||||
}
|
||||
_allUnits[unit.name] = unit;
|
||||
_baseToUnits[unit.baseUnit]?.add(unit.name);
|
||||
}
|
||||
}
|
||||
|
||||
UnitSpec operator [](String unit) {
|
||||
if (!_allUnits.containsKey(unit)) {
|
||||
throw ArgumentError("Unit not found:${unit}");
|
||||
}
|
||||
return _allUnits.get(unit);
|
||||
}
|
||||
|
||||
UnitSpec get(String unit) => this[unit];
|
||||
|
||||
String _converterFromCodeString(Number x, String codeString) {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln("final x = ${x};");
|
||||
buffer.writeln("main(){return $codeString;}");
|
||||
final code = buffer.toString();
|
||||
return code;
|
||||
}
|
||||
|
||||
Number _convertToBase(Number x, String fromUnit) {
|
||||
final unit = this[fromUnit];
|
||||
|
||||
if (unit.factorFromUnitToBase != null) {
|
||||
return x * (unit.factorFromUnitToBase as Number);
|
||||
}
|
||||
|
||||
if (unit.codeFromUnitToBase == null) {
|
||||
throw ArgumentError("Unit has no codeFromUnitToBase: $unit");
|
||||
}
|
||||
|
||||
final d4rt = D4rt();
|
||||
final completeSource = _converterFromCodeString(x, unit.codeFromUnitToBase as String);
|
||||
final ret = d4rt.execute(source: completeSource);
|
||||
return ret as Number;
|
||||
}
|
||||
|
||||
Number _convertFromBase(Number x, String toUnit) {
|
||||
final unit = this[toUnit];
|
||||
|
||||
if (unit.factorFromUnitToBase != null) {
|
||||
return x / (unit.factorFromUnitToBase as Number);
|
||||
}
|
||||
|
||||
if (unit.codeFromBaseToUnit == null) {
|
||||
throw ArgumentError("Unit has no codeFromBaseToUnit: $unit");
|
||||
}
|
||||
|
||||
final d4rt = D4rt();
|
||||
final completeSource = _converterFromCodeString(x, unit.codeFromBaseToUnit as String);
|
||||
final ret = d4rt.execute(source: completeSource);
|
||||
return ret as Number;
|
||||
}
|
||||
|
||||
Number convert(Number x, String fromUnit, String toUnit) {
|
||||
final xBase = _convertToBase(x, fromUnit);
|
||||
final xTo = _convertFromBase(xBase, toUnit);
|
||||
|
||||
return xTo;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,106 @@
|
|||
import 'package:d4rt/d4rt.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
abstract class SetUtils {
|
||||
static Object safeGet(Map<Object?, Object?> map, String key) {
|
||||
if (!map.containsKey(key)) {
|
||||
throw ArgumentError("Key not found: $key -- $map");
|
||||
}
|
||||
return map[key] ?? "Not possible!!!";
|
||||
}
|
||||
|
||||
static String stringValue(Map<Object?, Object?> map, String key) {
|
||||
return safeGet(map, key).toString();
|
||||
}
|
||||
|
||||
static List<Object?> listValue(Map<Object?, Object?> map, String key) {
|
||||
return safeGet(map, key) as List<Object?>;
|
||||
}
|
||||
|
||||
static Number numberValue(Map<Object?, Object?> map, String key) {
|
||||
return double.parse(stringValue(map, key));
|
||||
}
|
||||
}
|
||||
|
||||
typedef Number = double;
|
||||
|
||||
|
||||
class UnitSpec {
|
||||
final String name;
|
||||
final String baseUnit;
|
||||
final String symbol;
|
||||
final Number? factorFromUnitToBase;
|
||||
final String? codeFromUnitToBase;
|
||||
final String? codeFromBaseToUnit;
|
||||
|
||||
static final BASEUNIT="BASEUNIT";
|
||||
|
||||
UnitSpec({
|
||||
required this.name,
|
||||
required this.baseUnit,
|
||||
required this.symbol,
|
||||
this.factorFromUnitToBase,
|
||||
this.codeFromBaseToUnit,
|
||||
this.codeFromUnitToBase,
|
||||
});
|
||||
|
||||
factory UnitSpec.fromSet(Map<Object?, Object?> theSet) {
|
||||
String name = SetUtils.stringValue(theSet, "name");
|
||||
String symbol = SetUtils.stringValue(theSet, "symbol");
|
||||
|
||||
if( theSet.containsKey("isBase") ){
|
||||
return UnitSpec(name: name, baseUnit: BASEUNIT, symbol: symbol, factorFromUnitToBase: 1);
|
||||
}
|
||||
|
||||
String baseUnit = SetUtils.stringValue(theSet, "baseUnit");
|
||||
|
||||
if (theSet.containsKey("factor")) {
|
||||
Number factorFromUnitToBase = SetUtils.numberValue(theSet, "factor");
|
||||
return UnitSpec(
|
||||
name: name,
|
||||
baseUnit: baseUnit,
|
||||
symbol: symbol,
|
||||
factorFromUnitToBase: factorFromUnitToBase,
|
||||
);
|
||||
}
|
||||
else if( theSet.containsKey("toBase")) {
|
||||
String codeFromBaseToUnit = SetUtils.stringValue(
|
||||
theSet,
|
||||
"fromBase",
|
||||
);
|
||||
String codeFromUnitToBase = SetUtils.stringValue(
|
||||
theSet,
|
||||
"toBase",
|
||||
);
|
||||
|
||||
return UnitSpec(name: name,
|
||||
baseUnit: baseUnit,
|
||||
symbol: symbol,
|
||||
codeFromBaseToUnit: codeFromBaseToUnit,
|
||||
codeFromUnitToBase: codeFromUnitToBase);
|
||||
}
|
||||
else{
|
||||
throw ArgumentError( "Need factor or toBase/fromBase");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
static List<UnitSpec> fromArrayStringLiteral(String arrayStringLiteral) {
|
||||
var d4rt = D4rt();
|
||||
final buffer = StringBuffer();
|
||||
buffer.write("main(){ return $arrayStringLiteral; }");
|
||||
final code = buffer.toString();
|
||||
|
||||
final List<Object?> list = d4rt.execute(source: code);
|
||||
|
||||
final units = list.map((set) => UnitSpec.fromSet(set as Map));
|
||||
|
||||
return units.toList(growable: false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class VariableSpec {
|
||||
final String name;
|
||||
final String magnitude;
|
||||
|
|
@ -65,10 +165,10 @@ class Formula {
|
|||
List<String> inputVarNames() =>
|
||||
input.map((v) => v.name).toList(growable: false);
|
||||
|
||||
factory Formula.fromStringLiteral( String setStringLiteral ){
|
||||
factory Formula.fromStringLiteral(String setStringLiteral) {
|
||||
var d4rt = D4rt();
|
||||
final buffer = StringBuffer();
|
||||
buffer.write( "main(){ return $setStringLiteral; }");
|
||||
buffer.write("main(){ return $setStringLiteral; }");
|
||||
final code = buffer.toString();
|
||||
|
||||
final Map<Object?, Object?> setLiteral = d4rt.execute(source: code);
|
||||
|
|
@ -76,48 +176,34 @@ class Formula {
|
|||
return Formula.fromSet(setLiteral);
|
||||
}
|
||||
|
||||
static List<Formula> fromArrayStringLiteral( String arrayStringLiteral ){
|
||||
static List<Formula> fromArrayStringLiteral(String arrayStringLiteral) {
|
||||
var d4rt = D4rt();
|
||||
final buffer = StringBuffer();
|
||||
buffer.write( "main(){ return $arrayStringLiteral; }");
|
||||
buffer.write("main(){ return $arrayStringLiteral; }");
|
||||
final code = buffer.toString();
|
||||
|
||||
final List<Object?> list = d4rt.execute(source: code);
|
||||
|
||||
final formulas = list.map( (set) => Formula.fromSet(set as Map) );
|
||||
final formulas = list.map((set) => Formula.fromSet(set as Map));
|
||||
|
||||
return formulas.toList(growable: false);
|
||||
}
|
||||
|
||||
factory Formula.fromSet(Map<Object?, Object?> theSet) {
|
||||
|
||||
Object safeGet(Map<Object?, Object?> map, String key){
|
||||
if( !map.containsKey(key) ){
|
||||
throw ArgumentError( "Key not found: $key -- $map" );
|
||||
}
|
||||
return map[key] ?? "Not possible!!!";
|
||||
}
|
||||
|
||||
String stringValue(Map<Object?, Object?> map, String key){
|
||||
return safeGet(map, key).toString();
|
||||
}
|
||||
|
||||
List<Object?> listValue(Map<Object?, Object?> map, String key){
|
||||
return safeGet(map,key) as List<Object?>;
|
||||
}
|
||||
|
||||
VariableSpec parseVar(Map<Object?, Object?> varSpec) {
|
||||
String name = stringValue(varSpec, "name");
|
||||
String magnitude = stringValue(varSpec, "magnitude");
|
||||
String name = SetUtils.stringValue(varSpec, "name");
|
||||
String magnitude = SetUtils.stringValue(varSpec, "magnitude");
|
||||
return VariableSpec(name: name, magnitude: magnitude);
|
||||
}
|
||||
|
||||
String name = stringValue( theSet, "name" );
|
||||
final List<Object?> inputSet = listValue( theSet, "input");
|
||||
List<VariableSpec> input = inputSet.map( (v) => parseVar(v as Map)).toList(growable: false);
|
||||
String name = SetUtils.stringValue(theSet, "name");
|
||||
final List<Object?> inputSet = SetUtils.listValue(theSet, "input");
|
||||
List<VariableSpec> input = inputSet
|
||||
.map((v) => parseVar(v as Map))
|
||||
.toList(growable: false);
|
||||
Map<Object?, Object?> outputSet = theSet.get("output");
|
||||
VariableSpec output = parseVar(outputSet);
|
||||
String d4rtCode = theSet.get("d4rtCode");
|
||||
String d4rtCode = SetUtils.stringValue(theSet, "d4rtCode");
|
||||
|
||||
return Formula(
|
||||
name: name,
|
||||
|
|
|
|||
41
lib/units/distance.d4rt.units
Normal file
41
lib/units/distance.d4rt.units
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[
|
||||
{"name": "meter", "symbol": "m", "isBase": true},
|
||||
{"name": "inch", "symbol": "in", "baseUnit": "meter", "factor" : 0.0254 },
|
||||
{"name": "kilometer", "symbol": "km", "baseUnit": "meter", "factor": 1000},
|
||||
{
|
||||
"name": "nautical mile",
|
||||
"symbol": "Nm",
|
||||
"baseUnit": "meter",
|
||||
"factor": 1852,
|
||||
},
|
||||
{
|
||||
"name": "light-year",
|
||||
"symbol": "ly",
|
||||
"baseUnit": "meter",
|
||||
"factor": 9.461e15,
|
||||
},
|
||||
{
|
||||
"name": "astronomical unit",
|
||||
"symbol": "AU",
|
||||
"baseUnit": "meter",
|
||||
"factor": 1.496e11,
|
||||
},
|
||||
{"name": "rod", "symbol": "rd", "baseUnit": "meter", "factor": 5.0292},
|
||||
{"name": "chain", "symbol": "ch", "baseUnit": "meter", "factor": 20.1168},
|
||||
{"name": "furlong", "symbol": "fur", "baseUnit": "meter", "factor": 201.168},
|
||||
{"name": "league", "symbol": "lea", "baseUnit": "meter", "factor": 4828.032},
|
||||
{"name": "elbow", "symbol": "elb", "baseUnit": "meter", "factor": 0.4572},
|
||||
{"name": "hand", "symbol": "hh", "baseUnit": "meter", "factor": 0.1016},
|
||||
{"name": "cubit", "symbol": "cub", "baseUnit": "meter", "factor": 0.4572},
|
||||
{"name": "span", "symbol": "sp", "baseUnit": "meter", "factor": 0.2286},
|
||||
{"name": "fathom", "symbol": "ftm", "baseUnit": "meter", "factor": 1.8288},
|
||||
{"name": "finger", "symbol": "fng", "baseUnit": "meter", "factor": 0.0222},
|
||||
{
|
||||
"name": "Roman mile",
|
||||
"symbol": "mil.rom",
|
||||
"baseUnit": "meter",
|
||||
"factor": 1479.5,
|
||||
},
|
||||
{"name": "Chinese li", "symbol": "li", "baseUnit": "meter", "factor": 500},
|
||||
{"name": "Japanese ri", "symbol": "ri", "baseUnit": "meter", "factor": 3927}
|
||||
]
|
||||
74
lib/units/temperature.d4rt.units
Normal file
74
lib/units/temperature.d4rt.units
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
[
|
||||
{"name": "Kelvin", "symbol": "K", "isBase": true},
|
||||
{
|
||||
"name": "Celsius",
|
||||
"symbol": "°C",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "x + 273.15",
|
||||
"fromBase": "x - 273.15",
|
||||
},
|
||||
{
|
||||
"name": "Fahrenheit",
|
||||
"symbol": "°F",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "(x - 32) * 5/9 + 273.15",
|
||||
"fromBase": "(x - 273.15) * 9/5 + 32",
|
||||
},
|
||||
{
|
||||
"name": "Rankine",
|
||||
"symbol": "°R",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "x * 5/9",
|
||||
"fromBase": "x * 9/5",
|
||||
},
|
||||
{
|
||||
"name": "Réaumur",
|
||||
"symbol": "°Ré",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "x * 5/4 + 273.15",
|
||||
"fromBase": "(x - 273.15) * 4/5",
|
||||
},
|
||||
{
|
||||
"name": "Delisle",
|
||||
"symbol": "°D",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "373.15 - x * 2/3",
|
||||
"fromBase": "(373.15 - x) * 3/2",
|
||||
},
|
||||
{
|
||||
"name": "Rømer",
|
||||
"symbol": "°Rø",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "(x - 7.5) * 40/21 + 273.15",
|
||||
"fromBase": "(x - 273.15) * 21/40 + 7.5",
|
||||
},
|
||||
{
|
||||
"name": "Gas Mark",
|
||||
"symbol": "GM",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": """
|
||||
if (x < 1) {
|
||||
double celsius = (243 - 25 * (log(1 / x) / log(2))) / 1.8;
|
||||
return celsius + 273.15; // convert Celsius to Kelvin
|
||||
} else {
|
||||
double celsius = x * 14 + 121;
|
||||
return celsius + 273.15;
|
||||
}
|
||||
""",
|
||||
"fromBase": """
|
||||
double celsius = x - 273.15; // convert Kelvin to Celsius
|
||||
if (celsius < 135) { // corresponds to G < 1
|
||||
return pow(2, (1.8 * celsius - 243) / 25);
|
||||
} else {
|
||||
return (celsius - 121) / 14;
|
||||
}
|
||||
"""
|
||||
},
|
||||
{
|
||||
"name": "Solar Temperature",
|
||||
"symbol": "°Sol",
|
||||
"baseUnit": "Kelvin",
|
||||
"toBase": "x * 1000000",
|
||||
"fromBase": "x / 1000000",
|
||||
}
|
||||
]
|
||||
18
pubspec.lock
18
pubspec.lock
|
|
@ -66,7 +66,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
|
|
@ -171,6 +171,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -315,6 +323,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
resource_portable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: resource_portable
|
||||
sha256: "8ffa33b0769645e26d82c8f4e79adde3dd34b9e0bba981166d4e3798f39cffd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ dependencies:
|
|||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
resource_portable:
|
||||
d4rt:
|
||||
flutter_d4rt:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
|
||||
import 'package:d4rt_formulas/corpus.dart';
|
||||
import 'package:d4rt_formulas/formula_evaluator.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:d4rt_formulas/formula_models.dart';
|
||||
import 'dart:convert' show utf8;
|
||||
|
||||
import 'package:resource_portable/resource.dart' show Resource;
|
||||
|
||||
void main() {
|
||||
|
||||
|
|
@ -68,13 +72,13 @@ void main() {
|
|||
final setLiteral = {
|
||||
"name": "Newton's second law",
|
||||
"input": [
|
||||
{ "name": 'm', "magnitude": 'mass'},
|
||||
{ "name": 'a', "magnitude": 'acceleration'}
|
||||
{"name": 'm', "magnitude": 'mass'},
|
||||
{"name": 'a', "magnitude": 'acceleration'},
|
||||
],
|
||||
"output": { "name": 'F', "magnitude": 'force'},
|
||||
"output": {"name": 'F', "magnitude": 'force'},
|
||||
"d4rtCode": '''
|
||||
F = a * m;
|
||||
'''
|
||||
''',
|
||||
};
|
||||
|
||||
final formula = Formula.fromSet(setLiteral);
|
||||
|
|
@ -88,7 +92,7 @@ void main() {
|
|||
expect(result, 98.0); // F = m * a = 10 * 9.8 = 98 N
|
||||
});
|
||||
|
||||
test( 'd4rt parses formula from literal', (){
|
||||
test('d4rt parses formula from literal', () {
|
||||
final literal = """
|
||||
{
|
||||
"name": "Newton's second law",
|
||||
|
|
@ -112,11 +116,9 @@ void main() {
|
|||
});
|
||||
|
||||
expect(result, 98.0); // F = m * a = 10 * 9.8 = 98 N
|
||||
|
||||
});
|
||||
|
||||
|
||||
test( 'd4rt parses formula from list literal', (){
|
||||
test('d4rt parses formula from list literal', () {
|
||||
final literal = """
|
||||
[
|
||||
{
|
||||
|
|
@ -155,8 +157,5 @@ void main() {
|
|||
});
|
||||
|
||||
expect(result, 98.0); // F = m * a = 10 * 9.8 = 98 N
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue