Files
Rozpisky/Rozpisky/Dxf/LogaResolver.cs
T
marekandClaude Opus 5 cd2b82db3c Vlastní loga uživatele mimo instalační složku
Loga se dosud hledala jen v loga\ vedle .exe. Kdo si do číselníku přidal
firmu s vlastním logem, musel .dxf nakopírovat do instalační složky – tu ale
přeinstalace přepíše a v Program Files do ní nejde psát. Číselník firmu
přežil, logo ne.

Nově se hledá nejdřív v %APPDATA%\Rozpisky\loga a teprve pak mezi
dodávanými; nabídka v Údajích projektu je sjednocením obou složek (stejné
jméno se nezdvojuje). Tlačítko v Nastavení složku založí a otevře.

Hierarchie je stejná jako u číselníků a je to úmyslné: při konfliktu vyhrává
uživatelská vrstva. Kdyby vyhrávala dodávaná, uživatelova úprava by se tiše
neprojevila – on ji vidí uloženou, program pracuje s něčím jiným. Výjimkou
zůstávají data držící správnost výstupu (mapovani.json, Rozpiska.dxf), kde
vyhrává program.

Subagenti dostali explicitní model: render-dxf a wpf-ui sonnet,
verifikator haiku (jen spouští build a testy).

Build 0 chyb / 0 varování, 55 testů zelených (5 nových na slučování nabídky).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:13:13 +02:00

114 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using Rozpisky.Data;
using Rozpisky.Models;
namespace Rozpisky.Dxf;
/// <summary>
/// Sestaví mapu „hladina placeholderu → absolutní cesta k DXF" pro jednu rozpisku.
/// Pravidla (kap. 4.3 manuálu SŽ):
/// <list type="bullet">
/// <item><c>Rozpiska_logo1/2</c> financující instituce (dotační vlevo / státní vpravo),
/// projektové, soubor ze složky <c>loga\</c>,</item>
/// <item><c>Rozpiska_logo3</c> logo investora (číselník Organizace dle <c>Projekt.Investor</c>),</item>
/// <item><c>Rozpiska_logo4</c> logo zhotovitele díla (číselník Firmy dle <c>Projekt.ZhotovitelSOD</c>),</item>
/// <item><c>Rozpiska_logo5</c> logo zhotovitele objektu (číselník Firmy dle <c>Priloha.ZhotovitelCasti</c>),</item>
/// <item><c>Rozpiska_Orientační schéma</c> DXF u přílohy (<c>Priloha.SchemaSoubor</c>), relativně k .rzp.</item>
/// </list>
/// Položky bez vyplněného / neexistujícího souboru se vynechají.
/// <para>
/// Loga se hledají ve <b>dvou složkách</b> se stejnou logikou jako číselníky: nejdřív
/// <c>loga\</c> v uživatelské složce (viz <see cref="UzivatelskaData"/>), pak <c>loga\</c> vedle
/// .exe. Uživatelské logo stejného jména přebíjí dodávané. Bez toho by si uživatel, který přidá
/// do číselníku firmu s vlastním logem, musel soubor kopírovat do instalační složky tu ale
/// přeinstalace přepíše a v <c>Program Files</c> do ní nejde psát.
/// </para>
/// </summary>
public static class LogaResolver
{
/// <summary>Složka s logy dodávanými s programem (vedle .exe). Jen ke čtení instalace ji přepisuje.</summary>
public static string LogaDir => Path.Combine(AppContext.BaseDirectory, "loga");
/// <summary>Složka pro loga, která si přidal uživatel. Přeinstalace na ni nesahá.</summary>
public static string UzivatelskaLogaDir => UzivatelskaData.Cesta("loga");
/// <summary>Najde soubor loga: absolutní cesta se bere, jak je, jinak se hledá nejdřív mezi
/// uživatelskými logy a pak mezi dodávanými. Vrací null, když soubor nikde není.</summary>
public static string? Najdi(string? soubor)
{
if (string.IsNullOrWhiteSpace(soubor)) return null;
if (Path.IsPathRooted(soubor)) return File.Exists(soubor) ? soubor : null;
var uzivatelske = Path.Combine(UzivatelskaLogaDir, soubor);
if (File.Exists(uzivatelske)) return uzivatelske;
var dodavane = Path.Combine(LogaDir, soubor);
return File.Exists(dodavane) ? dodavane : null;
}
/// <summary>Jména dostupných log z obou složek dohromady, abecedně. Uživatelské logo stejného
/// jména dodávané nezdvojuje v nabídce je jednou a použije se to uživatelovo.</summary>
public static IReadOnlyList<string> DostupnaLoga() => SlozNabidku(UzivatelskaLogaDir, LogaDir);
/// <summary>Vlastní slučování nabídky s cestami v parametru, ať jde otestovat bez profilu uživatele.</summary>
internal static List<string> SlozNabidku(string uzivatelskaDir, string dodavanaDir)
{
var jmena = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var dir in new[] { dodavanaDir, uzivatelskaDir })
{
try
{
if (!Directory.Exists(dir)) continue;
foreach (var soubor in Directory.GetFiles(dir, "*.dxf"))
jmena.Add(Path.GetFileName(soubor));
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Nedostupná složka (odpojený síťový disk apod.) nesmí shodit nabídku.
}
}
return jmena.ToList();
}
/// <param name="rzpDir">Složka aktuálního souboru .rzp (pro relativní cestu k schématu); null = neuloženo.</param>
public static IReadOnlyDictionary<string, string> Resolve(Projekt p, Priloha r, Ciselniky c, string? rzpDir)
{
var d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
PridejZeSlozky(d, "Rozpiska_logo1", p.Logo1Soubor);
PridejZeSlozky(d, "Rozpiska_logo2", p.Logo2Soubor);
PridejZeSlozky(d, "Rozpiska_logo3", OrganizaceLogo(c, p.Investor));
PridejZeSlozky(d, "Rozpiska_logo4", FirmaLogo(c, p.ZhotovitelSOD));
PridejZeSlozky(d, "Rozpiska_logo5", FirmaLogo(c, r.ZhotovitelCasti));
var schema = ResolveSchema(r.SchemaSoubor, rzpDir);
if (schema is not null) d["Rozpiska_Orientační schéma"] = schema;
return d;
}
private static void PridejZeSlozky(IDictionary<string, string> d, string hladina, string? soubor)
{
var cesta = Najdi(soubor);
if (cesta is not null) d[hladina] = cesta;
}
/// <summary>Cesta ke schématu: absolutní beze změny, jinak relativně ke složce .rzp.</summary>
private static string? ResolveSchema(string? soubor, string? rzpDir)
{
if (string.IsNullOrWhiteSpace(soubor)) return null;
if (Path.IsPathRooted(soubor)) return File.Exists(soubor) ? soubor : null;
if (string.IsNullOrEmpty(rzpDir)) return null;
var cesta = Path.Combine(rzpDir, soubor);
return File.Exists(cesta) ? cesta : null;
}
private static string? FirmaLogo(Ciselniky c, string? nazev)
=> string.IsNullOrWhiteSpace(nazev) ? null
: c.Firmy.FirstOrDefault(f => string.Equals(f.Nazev, nazev, StringComparison.OrdinalIgnoreCase))?.Logo;
private static string? OrganizaceLogo(Ciselniky c, string? nazev)
=> string.IsNullOrWhiteSpace(nazev) ? null
: c.Organizace.FirstOrDefault(o => string.Equals(o.Nazev, nazev, StringComparison.OrdinalIgnoreCase))?.Logo;
}