Added new tiles and extended model

This commit is contained in:
Tim Lappe 2025-05-28 09:04:15 +02:00
parent 593972c401
commit 3160d60eaf
38 changed files with 751 additions and 85 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace App\Domain\Model\Battery;
use App\Domain\Model\Value\Energy;
class BatteryProperties
{
public function __construct(
public readonly Energy $usableCapacity,
public readonly Energy $totalCapacity,
public readonly CellChemistry $cellChemistry = CellChemistry::LithiumIronPhosphate,
public readonly string $model = '4680',
public readonly string $manufacturer = 'Tesla',
) {
}
public function __toString(): string
{
return $this->usableCapacity->__toString();
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Domain\Model\Battery;
enum CellChemistry: string
{
case LithiumIronPhosphate = 'LFP';
case LithiumNickelCobaltAluminumOxide = 'NCA';
case LithiumNickelManganeseOxide = 'NMC';
}

View File

@ -4,21 +4,8 @@ namespace App\Domain\Model;
class CarModel
{
private ?string $id = null;
private string $name;
private int $productionStartYear;
private ?int $productionEndYear = null;
private ?string $category = null;
private ?string $description = null;
private ?string $image = null;
private ?Brand $brand = null;
private array $revisions = [];
public function __construct(
public string $name,
public ?Brand $brand = null,
) {}
}

View File

@ -2,29 +2,31 @@
namespace App\Domain\Model;
use App\Domain\Model\Value\Acceleration;
use App\Domain\Model\Value\Battery;
use App\Domain\Model\Value\Consumption;
use App\Domain\Model\Value\ChargingSpeed;
use App\Domain\Model\Value\Power;
use App\Domain\Model\Value\Range;
use App\Domain\Model\Value\TopSpeed;
use App\Domain\Model\Value\Date;
use App\Domain\Model\Value\Price;
use App\Domain\Model\Battery\BatteryProperties;
use App\Domain\Model\Charging\ChargingProperties;
final readonly class CarRevision
{
public function __construct(
public string $name,
public int $releaseYear,
public ?Power $power = null,
public ?Acceleration $acceleration = null,
public ?TopSpeed $topSpeed = null,
public ?Range $range = null,
public ?Battery $battery = null,
public ?Consumption $consumption = null,
public ?Price $price = null,
public ?ChargingSpeed $chargingSpeed = null,
public ?Date $productionBegin = null,
public ?Date $productionEnd = null,
public ?DrivingCharacteristics $drivingCharacteristics = null,
public ?BatteryProperties $battery = null,
public ?ChargingProperties $chargingProperties = null,
public ?RangeProperties $rangeProperties = null,
public ?Price $catalogPrice = null,
public ?CarModel $carModel = null,
public ?Image $image = null,
) {}
) {
if ($this->productionBegin && $this->productionEnd) {
if ($this->productionBegin->year > $this->productionEnd->year) {
throw new \InvalidArgumentException('Production begin year must be before production end year');
}
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Domain\Model\Charging;
use App\Domain\Model\Value\Power;
final readonly class ChargeCurve
{
public function __construct(
public ?Power $averagePowerSoc0 = null,
public ?Power $averagePowerSoc10 = null,
public ?Power $averagePowerSoc20 = null,
public ?Power $averagePowerSoc30 = null,
public ?Power $averagePowerSoc40 = null,
public ?Power $averagePowerSoc50 = null,
public ?Power $averagePowerSoc60 = null,
public ?Power $averagePowerSoc70 = null,
public ?Power $averagePowerSoc80 = null,
public ?Power $averagePowerSoc90 = null,
public ?Power $averagePowerSoc100 = null,
) {}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Domain\Model\Charging;
final readonly class ChargeTimeProperties
{
public function __construct(
public ?int $minutesFrom0To100 = null,
public ?int $minutesFrom0To70 = null,
public ?int $minutesFrom10To70 = null,
public ?int $minutesFrom20To70 = null,
public ?int $minutesFrom10To80 = null,
public ?int $minutesFrom20To80 = null,
public ?int $minutesFrom10To90 = null,
public ?int $minutesFrom20To90 = null,
) {}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Domain\Model\Charging;
final readonly class ChargingConnectivity
{
/**
* @param ConnectorType[] $connectorTypes
*/
public function __construct(
public readonly ?bool $is400v = null,
public readonly ?bool $is800v = null,
public readonly ?bool $plugAndCharge = null,
public readonly array $connectorTypes = [],
) {}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Domain\Model\Charging;
use App\Domain\Model\Value\Power;
final readonly class ChargingProperties
{
public function __construct(
public ?Power $topChargingSpeed = null,
public ?ChargeCurve $chargeCurve = null,
public ?ChargeTimeProperties $chargeTimeProperties = null,
public ?ChargingConnectivity $chargingConnectivity = null,
) {
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Domain\Model\Charging;
enum ConnectorType: string
{
case Type2 = 'Type 2';
case CCS = 'CCS';
case CHAdeMO = 'CHAdeMO';
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Domain\Model;
use App\Domain\Model\Value\Power;
use App\Domain\Model\Value\Acceleration;
use App\Domain\Model\Value\Consumption;
use App\Domain\Model\Value\Speed;
final readonly class DrivingCharacteristics
{
public function __construct(
public ?Power $power = null,
public ?Acceleration $acceleration = null,
public ?Speed $topSpeed = null,
public ?Consumption $consumption = null,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Model\Range;
use App\Domain\Model\Value\Range;
final readonly class NefzRange
{
public function __construct(
public readonly Range $range,
) {}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Domain\Model\Range;
use App\Domain\Model\Value\Range;
use App\Domain\Model\Value\Season;
use App\Domain\Model\Value\Speed;
class RealRange
{
public function __construct(
public readonly Range $range,
public readonly ?Season $season = null,
public readonly ?Speed $averageSpeed = null,
)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Model\Range;
use App\Domain\Model\Value\Range;
final readonly class WltpRange
{
public function __construct(
public readonly Range $range,
) {}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Domain\Model;
use App\Domain\Model\Range\NefzRange;
use App\Domain\Model\Range\WltpRange;
final readonly class RangeProperties
{
/**
* @param RealRange[] $realRangeTests
*/
public function __construct(
public readonly ?WltpRange $wltp = null,
public readonly ?NefzRange $nefz = null,
public readonly array $realRangeTests = [],
) {}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Domain\Model\Value;
class Battery
{
public function __construct(
public readonly Energy $usableCapacity,
public readonly Energy $totalCapacity,
public readonly string $technology = 'Lithium-Ion',
) {
}
public function __toString(): string
{
return $this->usableCapacity->__toString();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Domain\Model\Value;
final readonly class Date
{
public function __construct(
public ?int $month,
public ?int $day,
public int $year,
) {
if ($this->month < 1 || $this->month > 12) {
throw new \InvalidArgumentException('Month must be between 1 and 12');
}
if ($this->day < 1 || $this->day > 31) {
throw new \InvalidArgumentException('Day must be between 1 and 31');
}
}
public function __toString(): string
{
return $this->year . '-' . $this->month . '-' . $this->day;
}
}

View File

@ -7,6 +7,7 @@ class Price
public function __construct(
public readonly int $price,
public readonly Currency $currency,
public readonly ?bool $includesVat = null,
) {}
public function __toString(): string

View File

@ -0,0 +1,11 @@
<?php
namespace App\Domain\Model\Value;
enum Season: string
{
case Winter = 'Winter';
case Spring = 'Spring';
case Summer = 'Summer';
case Autumn = 'Autumn';
}

View File

@ -2,7 +2,7 @@
namespace App\Domain\Model\Value;
class TopSpeed
class Speed
{
public function __construct(
public readonly int $kmh,

View File

@ -3,18 +3,30 @@
namespace App\Domain\Search;
use App\Domain\Model\CarRevision;
use App\Domain\Model\DrivingCharacteristics;
use App\Domain\Model\Image;
use App\Domain\Model\Value\Currency;
use App\Domain\Model\Value\Date;
use App\Domain\Model\Value\Price;
use App\Domain\Model\Value\Range;
use App\Domain\Model\Value\Battery;
use App\Domain\Model\Battery\BatteryProperties;
use App\Domain\Model\Battery\CellChemistry;
use App\Domain\Model\Value\Power;
use App\Domain\Model\Value\Acceleration;
use App\Domain\Model\Value\ChargingSpeed;
use App\Domain\Model\Value\Consumption;
use App\Domain\Model\Value\TopSpeed;
use App\Domain\Model\Value\Speed;
use App\Domain\Model\Value\Drivetrain;
use App\Domain\Model\Value\Energy;
use App\Domain\Model\Charging\ChargingProperties;
use App\Domain\Model\Charging\ChargeCurve;
use App\Domain\Model\Charging\ChargeTimeProperties;
use App\Domain\Model\Charging\ChargingConnectivity;
use App\Domain\Model\Charging\ConnectorType;
use App\Domain\Model\RangeProperties;
use App\Domain\Model\Range\WltpRange;
use App\Domain\Model\Range\NefzRange;
use App\Domain\Model\Range\RealRange;
use App\Domain\Model\Value\Season;
use App\Domain\Search\Tiles\SectionTile;
use App\Domain\Search\Tiles\SubSectionTile;
use App\Domain\Search\Tiles\BrandTile;
@ -29,45 +41,129 @@ use App\Domain\Search\Tiles\AvailabilityTile;
use App\Domain\Search\Tiles\CarTile;
use App\Domain\Search\Tiles\TopSpeedTile;
use App\Domain\Search\Tiles\DrivetrainTile;
// New tiles
use App\Domain\Search\Tiles\ChargeCurveTile;
use App\Domain\Search\Tiles\ChargeTimeTile;
use App\Domain\Search\Tiles\ChargingConnectivityTile;
use App\Domain\Search\Tiles\BatteryDetailsTile;
use App\Domain\Search\Tiles\ProductionPeriodTile;
use App\Domain\Search\Tiles\RangeComparisonTile;
use App\Domain\Search\Tiles\RealRangeTile;
use App\Domain\Search\Tiles\PerformanceOverviewTile;
class Engine
{
public function search(string $query): TileCollection
{
// Create comprehensive test data showing all the new features
$batteryProperties = new BatteryProperties(
usableCapacity: new Energy(77.0),
totalCapacity: new Energy(82.0),
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
model: 'NCM811',
manufacturer: 'LG Energy Solution'
);
$chargeCurve = new ChargeCurve(
averagePowerSoc0: new Power(175),
averagePowerSoc10: new Power(175),
averagePowerSoc20: new Power(170),
averagePowerSoc30: new Power(165),
averagePowerSoc40: new Power(155),
averagePowerSoc50: new Power(145),
averagePowerSoc60: new Power(130),
averagePowerSoc70: new Power(110),
averagePowerSoc80: new Power(85),
averagePowerSoc90: new Power(50),
averagePowerSoc100: new Power(20)
);
$chargeTimeProperties = new ChargeTimeProperties(
minutesFrom0To100: 155,
minutesFrom10To80: 28,
minutesFrom20To80: 25,
minutesFrom10To90: 42
);
$chargingConnectivity = new ChargingConnectivity(
is400v: true,
is800v: false,
plugAndCharge: true,
connectorTypes: [ConnectorType::CCS, ConnectorType::Type2]
);
$chargingProperties = new ChargingProperties(
topChargingSpeed: new Power(175),
chargeCurve: $chargeCurve,
chargeTimeProperties: $chargeTimeProperties,
chargingConnectivity: $chargingConnectivity
);
$drivingCharacteristics = new DrivingCharacteristics(
power: new Power(210),
acceleration: new Acceleration(6.6),
topSpeed: new Speed(180),
consumption: new Consumption(new Energy(17.1))
);
$wltpRange = new WltpRange(new Range(450));
$nefzRange = new NefzRange(new Range(485));
$realRangeTests = [
new RealRange(new Range(380), Season::Winter, new Speed(130)),
new RealRange(new Range(420), Season::Summer, new Speed(120)),
new RealRange(new Range(365), Season::Winter, new Speed(160))
];
$rangeProperties = new RangeProperties(
wltp: $wltpRange,
nefz: $nefzRange,
realRangeTests: $realRangeTests
);
$skodaElroq85 = new CarRevision(
'Skoda Elroq 85', 2024,
new Power(210),
new Acceleration(6.6),
new TopSpeed(180),
new Range(450),
new Battery(new Energy(77.0), new Energy(82.0)),
new Consumption(new Energy(171)),
new Price(43900, Currency::euro()),
new ChargingSpeed(new Power(175), new Power(11)),
image: new Image('https://www.scherer-gruppe.de/media/f3b72d42-4b26-4606-8df4-d840efeff017/01_elroc.jpg?w=1920&h=758&action=crop&scale=both&anchor=middlecenter'),
name: 'Skoda Enyaq iV 85',
productionBegin: new Date(1, 1, 2020),
productionEnd: null, // Still in production
drivingCharacteristics: $drivingCharacteristics,
battery: $batteryProperties,
chargingProperties: $chargingProperties,
rangeProperties: $rangeProperties,
catalogPrice: new Price(43900, Currency::euro()),
image: new Image('https://www.scherer-gruppe.de/media/f3b72d42-4b26-4606-8df4-d840efeff017/01_elroc.jpg?w=1920&h=758&action=crop&scale=both&anchor=middlecenter')
);
return new TileCollection([
new SectionTile('Skoda Elroq 85', [
new SectionTile('Skoda Enyaq iV 85', [
new CarTile($skodaElroq85->image, [
new BrandTile('Skoda'),
new PriceTile(new Price(43900, Currency::euro())),
new AvailabilityTile('Bestellbar', 'Oktober 2024'),
new RangeTile(new Range(450)),
new PriceTile($skodaElroq85->catalogPrice),
new ProductionPeriodTile($skodaElroq85->productionBegin, $skodaElroq85->productionEnd),
]),
new SubSectionTile('Performance', [
new PowerTile(new Power(210)),
new AccelerationTile(new Acceleration(6.6)),
new TopSpeedTile(new TopSpeed(180)),
new DrivetrainTile(new Drivetrain('rear')),
], 'Motor and driving performance specifications'),
new SubSectionTile('Range & Efficiency', [
new RangeTile(new Range(450)),
new BatteryTile(new Battery(new Energy(77.0), new Energy(82.0))),
new ConsumptionTile(new Consumption(new Energy(171))),
new ChargingTile(new ChargingSpeed(new Power(175), new Power(11))),
], 'Battery capacity, range, and charging capabilities'),
new SubSectionTile('Performance', [
new PowerTile($drivingCharacteristics->power),
new AccelerationTile($drivingCharacteristics->acceleration),
new TopSpeedTile($drivingCharacteristics->topSpeed),
new DrivetrainTile(new Drivetrain('rear')),
new ConsumptionTile($drivingCharacteristics->consumption),
], 'Individual performance metrics'),
new SubSectionTile('Reichweite', [
new RangeTile($wltpRange->range),
new RangeComparisonTile($skodaElroq85->rangeProperties),
new RealRangeTile($realRangeTests),
], 'Range data from different sources'),
new SubSectionTile('Batterie', [
new BatteryTile($skodaElroq85->battery),
new BatteryDetailsTile($skodaElroq85->battery),
], 'Battery capacity and technology'),
new SubSectionTile('Laden', [
new ChargeTimeTile($chargingProperties->chargeTimeProperties),
new ChargingConnectivityTile($chargingProperties->chargingConnectivity),
new ChargeCurveTile($chargingProperties->chargeCurve),
], 'Charging capabilities and compatibility'),
]),
]);
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Battery\BatteryProperties;
final readonly class BatteryDetailsTile
{
public function __construct(
public BatteryProperties $batteryProperties,
) {}
}

View File

@ -2,11 +2,11 @@
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Value\Battery;
use App\Domain\Model\Battery\BatteryProperties;
class BatteryTile
{
public function __construct(
public readonly Battery $battery,
public readonly BatteryProperties $battery,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Charging\ChargeCurve;
final readonly class ChargeCurveTile
{
public function __construct(
public ChargeCurve $chargeCurve,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Charging\ChargeTimeProperties;
final readonly class ChargeTimeTile
{
public function __construct(
public ChargeTimeProperties $chargeTimeProperties,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Charging\ChargingConnectivity;
final readonly class ChargingConnectivityTile
{
public function __construct(
public ChargingConnectivity $chargingConnectivity,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\DrivingCharacteristics;
final readonly class PerformanceOverviewTile
{
public function __construct(
public DrivingCharacteristics $drivingCharacteristics,
) {}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Value\Date;
final readonly class ProductionPeriodTile
{
public function __construct(
public ?Date $productionBegin = null,
public ?Date $productionEnd = null,
) {}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Domain\Search\Tiles;
use App\Domain\Model\RangeProperties;
final readonly class RangeComparisonTile
{
public function __construct(
public RangeProperties $rangeProperties,
) {}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Domain\Search\Tiles;
final readonly class RealRangeTile
{
/**
* @param \App\Domain\Model\Range\RealRange[] $realRangeTests
*/
public function __construct(
public array $realRangeTests,
) {}
}

View File

@ -2,11 +2,11 @@
namespace App\Domain\Search\Tiles;
use App\Domain\Model\Value\TopSpeed;
use App\Domain\Model\Value\Speed;
class TopSpeedTile
{
public function __construct(
public readonly TopSpeed $topSpeed,
public readonly Speed $topSpeed,
) {}
}

View File

@ -0,0 +1,21 @@
<div class="tile battery-details-tile">
<div class="tile-title">Zellchemie</div>
<div class="battery-info" style="margin-top: 12px; text-align: center;">
<!-- Chemistry Badge -->
{% set chemistryColor = tile.batteryProperties.cellChemistry.value == 'LFP' ? '#28a745' : (tile.batteryProperties.cellChemistry.value == 'NMC' ? '#007acc' : '#6f42c1') %}
<div style="margin-bottom: 12px;">
<span style="background: {{ chemistryColor }}; color: white; padding: 6px 12px; border-radius: 12px; font-size: 14px; font-weight: bold;">
{{ tile.batteryProperties.cellChemistry.value }}
</span>
</div>
<!-- Manufacturer & Model -->
{% if tile.batteryProperties.manufacturer != 'Tesla' or tile.batteryProperties.model != '4680' %}
<div style="font-size: 12px; color: #666;">
{{ tile.batteryProperties.manufacturer }}<br>
{{ tile.batteryProperties.model }}
</div>
{% endif %}
</div>
<small style="color: #666;">Batterietechnologie</small>
</div>

View File

@ -0,0 +1,74 @@
<div class="tile charge-curve-tile" style="grid-column: span 3; grid-row: span 2;">
<div class="tile-title">Ladekurve</div>
<div class="charge-curve-container" style="position: relative; height: 120px; margin-top: 10px;">
<svg width="100%" height="100%" viewBox="0 0 300 100" style="overflow: visible;">
<!-- Grid lines -->
{% for i in 0..10 %}
<line x1="{{ i * 30 }}" y1="0" x2="{{ i * 30 }}" y2="100"
stroke="#f0f0f0" stroke-width="0.5"/>
{% endfor %}
{% for i in 0..5 %}
<line x1="0" y1="{{ i * 20 }}" x2="300" y2="{{ i * 20 }}"
stroke="#f0f0f0" stroke-width="0.5"/>
{% endfor %}
<!-- Charge curve path -->
<path d="M0,{{ 100 - ((tile.chargeCurve.averagePowerSoc0 ? tile.chargeCurve.averagePowerSoc0.kilowatts : 0) / 200 * 80) }}
L30,{{ 100 - ((tile.chargeCurve.averagePowerSoc10 ? tile.chargeCurve.averagePowerSoc10.kilowatts : 0) / 200 * 80) }}
L60,{{ 100 - ((tile.chargeCurve.averagePowerSoc20 ? tile.chargeCurve.averagePowerSoc20.kilowatts : 0) / 200 * 80) }}
L90,{{ 100 - ((tile.chargeCurve.averagePowerSoc30 ? tile.chargeCurve.averagePowerSoc30.kilowatts : 0) / 200 * 80) }}
L120,{{ 100 - ((tile.chargeCurve.averagePowerSoc40 ? tile.chargeCurve.averagePowerSoc40.kilowatts : 0) / 200 * 80) }}
L150,{{ 100 - ((tile.chargeCurve.averagePowerSoc50 ? tile.chargeCurve.averagePowerSoc50.kilowatts : 0) / 200 * 80) }}
L180,{{ 100 - ((tile.chargeCurve.averagePowerSoc60 ? tile.chargeCurve.averagePowerSoc60.kilowatts : 0) / 200 * 80) }}
L210,{{ 100 - ((tile.chargeCurve.averagePowerSoc70 ? tile.chargeCurve.averagePowerSoc70.kilowatts : 0) / 200 * 80) }}
L240,{{ 100 - ((tile.chargeCurve.averagePowerSoc80 ? tile.chargeCurve.averagePowerSoc80.kilowatts : 0) / 200 * 80) }}
L270,{{ 100 - ((tile.chargeCurve.averagePowerSoc90 ? tile.chargeCurve.averagePowerSoc90.kilowatts : 0) / 200 * 80) }}
L300,{{ 100 - ((tile.chargeCurve.averagePowerSoc100 ? tile.chargeCurve.averagePowerSoc100.kilowatts : 0) / 200 * 80) }}"
fill="none" stroke="#007acc" stroke-width="2" opacity="0.8"/>
<!-- Peak power indicator -->
{% set maxPower = 0 %}
{% if tile.chargeCurve.averagePowerSoc0 and tile.chargeCurve.averagePowerSoc0.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc0.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc10 and tile.chargeCurve.averagePowerSoc10.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc10.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc20 and tile.chargeCurve.averagePowerSoc20.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc20.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc30 and tile.chargeCurve.averagePowerSoc30.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc30.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc40 and tile.chargeCurve.averagePowerSoc40.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc40.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc50 and tile.chargeCurve.averagePowerSoc50.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc50.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc60 and tile.chargeCurve.averagePowerSoc60.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc60.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc70 and tile.chargeCurve.averagePowerSoc70.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc70.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc80 and tile.chargeCurve.averagePowerSoc80.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc80.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc90 and tile.chargeCurve.averagePowerSoc90.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc90.kilowatts %}
{% endif %}
{% if tile.chargeCurve.averagePowerSoc100 and tile.chargeCurve.averagePowerSoc100.kilowatts > maxPower %}
{% set maxPower = tile.chargeCurve.averagePowerSoc100.kilowatts %}
{% endif %}
{% if maxPower > 0 %}
<text x="5" y="15" font-size="10" fill="#666">{{ maxPower|round }}kW</text>
{% endif %}
<text x="5" y="95" font-size="10" fill="#666">0kW</text>
<text x="5" y="105" font-size="8" fill="#999">0%</text>
<text x="285" y="105" font-size="8" fill="#999">100%</text>
</svg>
</div>
<small style="color: #666; font-size: 11px;">Ladeleistung über Batteriestand (SOC)</small>
</div>

View File

@ -0,0 +1,19 @@
<div class="tile charge-time-tile">
<div class="tile-title">Schnellladen</div>
<div class="charge-times" style="margin-top: 12px;">
{% if tile.chargeTimeProperties.minutesFrom10To80 %}
<div style="text-align: center; margin-bottom: 12px;">
<div style="font-weight: bold; font-size: 24px; color: #007acc;">{{ tile.chargeTimeProperties.minutesFrom10To80 }}</div>
<small style="color: #666; font-weight: 600;">Minuten 10-80%</small>
</div>
{% endif %}
{% if tile.chargeTimeProperties.minutesFrom20To80 %}
<div style="text-align: center;">
<div style="font-weight: bold; font-size: 18px; color: #6c757d;">{{ tile.chargeTimeProperties.minutesFrom20To80 }}</div>
<small style="color: #666;">Minuten 20-80%</small>
</div>
{% endif %}
</div>
<small style="color: #666;">DC Laden</small>
</div>

View File

@ -0,0 +1,35 @@
<div class="tile charging-connectivity-tile">
<div class="tile-title">Ladekompatibilität</div>
<div class="connectivity-features" style="margin-top: 8px;">
<!-- Voltage Support -->
<div style="display: flex; gap: 6px; margin-bottom: 6px;">
{% if tile.chargingConnectivity.is400v %}
<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 12px; font-size: 10px; font-weight: bold;">400V</span>
{% endif %}
{% if tile.chargingConnectivity.is800v %}
<span style="background: #007acc; color: white; padding: 2px 6px; border-radius: 12px; font-size: 10px; font-weight: bold;">800V</span>
{% endif %}
</div>
<!-- Plug & Charge -->
{% if tile.chargingConnectivity.plugAndCharge %}
<div style="margin-bottom: 6px;">
<span style="background: rgba(40, 167, 69, 0.1); color: #28a745; padding: 3px 8px; border-radius: 4px; font-size: 11px;">
⚡ Plug & Charge
</span>
</div>
{% endif %}
<!-- Connector Types -->
{% if tile.chargingConnectivity.connectorTypes|length > 0 %}
<div class="connector-types" style="display: flex; gap: 4px; flex-wrap: wrap;">
{% for connector in tile.chargingConnectivity.connectorTypes %}
<span style="background: rgba(108,117,125,0.1); color: #6c757d; padding: 2px 6px; border-radius: 8px; font-size: 10px; font-weight: 500;">
{{ connector.value }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
<small style="color: #666;">Ladeschnittstellen & Features</small>
</div>

View File

@ -0,0 +1,40 @@
<div class="tile performance-overview-tile" style="grid-column: span 3; grid-row: span 2;">
<div class="tile-title">Performance-Übersicht</div>
<div class="performance-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 12px; height: calc(100% - 40px);">
<!-- Power -->
{% if tile.drivingCharacteristics.power %}
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, rgba(0,122,204,0.1), rgba(0,122,204,0.05)); border-radius: 8px; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 24px; font-weight: bold; color: #007acc; margin-bottom: 4px;">{{ tile.drivingCharacteristics.power.kilowatts|round }}</div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">kW</div>
<small style="color: #666; font-weight: 600;">Leistung</small>
</div>
{% endif %}
<!-- Top Speed -->
{% if tile.drivingCharacteristics.topSpeed %}
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, rgba(255,193,7,0.1), rgba(255,193,7,0.05)); border-radius: 8px; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 24px; font-weight: bold; color: #ffc107; margin-bottom: 4px;">{{ tile.drivingCharacteristics.topSpeed.kmh }}</div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">km/h</div>
<small style="color: #666; font-weight: 600;">Höchstgeschwindigkeit</small>
</div>
{% endif %}
<!-- Acceleration -->
{% if tile.drivingCharacteristics.acceleration %}
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, rgba(40,167,69,0.1), rgba(40,167,69,0.05)); border-radius: 8px; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 24px; font-weight: bold; color: #28a745; margin-bottom: 4px;">{{ tile.drivingCharacteristics.acceleration.secondsFrom0To100 }}</div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">sec</div>
<small style="color: #666; font-weight: 600;">0-100 km/h</small>
</div>
{% endif %}
<!-- Consumption -->
{% if tile.drivingCharacteristics.consumption %}
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, rgba(111,66,193,0.1), rgba(111,66,193,0.05)); border-radius: 8px; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 20px; font-weight: bold; color: #6f42c1; margin-bottom: 4px;">{{ tile.drivingCharacteristics.consumption.energyPerKm.kwh|round(1) }}</div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">kWh/100km</div>
<small style="color: #666; font-weight: 600;">Verbrauch</small>
</div>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,38 @@
<div class="tile production-period-tile">
<div class="tile-title">Produktionszeitraum</div>
<div class="production-timeline" style="margin-top: 8px;">
{% if tile.productionBegin or tile.productionEnd %}
<div style="display: flex; align-items: center; gap: 8px;">
{% if tile.productionBegin %}
<div style="text-align: center;">
<div style="font-weight: bold; font-size: 16px; color: #28a745;">{{ tile.productionBegin.year }}</div>
<small style="color: #666;">Start</small>
</div>
{% endif %}
{% if tile.productionBegin and tile.productionEnd %}
<div style="flex: 1; height: 2px; background: linear-gradient(to right, #28a745, #dc3545); margin: 0 4px;"></div>
{% elseif tile.productionBegin %}
<div style="flex: 1; height: 2px; background: linear-gradient(to right, #28a745, #007acc); margin: 0 4px;"></div>
{% endif %}
{% if tile.productionEnd %}
<div style="text-align: center;">
<div style="font-weight: bold; font-size: 16px; color: #dc3545;">{{ tile.productionEnd.year }}</div>
<small style="color: #666;">Ende</small>
</div>
{% elseif tile.productionBegin %}
<div style="text-align: center;">
<div style="font-weight: bold; font-size: 16px; color: #007acc;">laufend</div>
<small style="color: #666;">aktuell</small>
</div>
{% endif %}
</div>
{% else %}
<div style="text-align: center; color: #666; font-style: italic;">
Zeitraum unbekannt
</div>
{% endif %}
</div>
<small style="color: #666;">Verfügbarkeit</small>
</div>

View File

@ -0,0 +1,28 @@
<div class="tile range-comparison-tile">
<div class="tile-title">Reichweiten-Vergleich</div>
<div class="range-comparison" style="margin-top: 8px;">
<!-- WLTP Range -->
{% if tile.rangeProperties.wltp %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(0,122,204,0.1); border-radius: 6px;">
<span style="font-weight: 600; color: #007acc;">WLTP</span>
<span style="font-weight: bold; font-size: 18px;">{{ tile.rangeProperties.wltp.range.kilometers }} km</span>
</div>
{% endif %}
<!-- NEFZ Range -->
{% if tile.rangeProperties.nefz %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(108,117,125,0.1); border-radius: 6px;">
<span style="font-weight: 600; color: #6c757d;">NEFZ</span>
<span style="font-weight: bold; font-size: 18px;">{{ tile.rangeProperties.nefz.range.kilometers }} km</span>
</div>
{% endif %}
<!-- Real Tests Summary -->
{% if tile.rangeProperties.realRangeTests|length > 0 %}
<div style="margin-top: 10px; padding: 4px 6px; background: rgba(40,167,69,0.05); border-radius: 4px; text-align: center;">
<small style="color: #28a745; font-weight: 600;">{{ tile.rangeProperties.realRangeTests|length }} Real-Tests verfügbar</small>
</div>
{% endif %}
</div>
<small style="color: #666;">Offizielle Testverfahren</small>
</div>

View File

@ -0,0 +1,18 @@
<div class="tile real-range-tile">
<div class="tile-title">Praxis-Tests</div>
<div class="real-tests" style="margin-top: 8px;">
{% for realTest in tile.realRangeTests|slice(0, 3) %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; padding: 4px 8px; background: rgba(40,167,69,0.1); border-radius: 4px; font-size: 13px;">
<span style="color: #28a745;">
{% if realTest.season %}{{ realTest.season.value }}{% endif %}
{% if realTest.averageSpeed %}{{ realTest.averageSpeed }}{% endif %}
</span>
<span style="font-weight: bold;">{{ realTest.range.kilometers }} km</span>
</div>
{% endfor %}
{% if tile.realRangeTests|length > 3 %}
<small style="color: #666; font-style: italic; text-align: center; display: block;">+{{ tile.realRangeTests|length - 3 }} weitere</small>
{% endif %}
</div>
<small style="color: #666;">Real-World Reichweite</small>
</div>