Newer
Older
EARS-LINUX / program / JacketData.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;

/// One chip position. Mirrors Form2.Circle.
public class Circle
{
    public float Number    { get; set; }
    public int   X         { get; set; }   // screen-space: OriginalX * 50 + MarginX
    public int   Y         { get; set; }
    public float OriginalX { get; set; }
    public float OriginalY { get; set; }
    public float OriginalZ { get; set; }
    public bool  Inner     { get; set; }   // non-integer Number
}

/// Loads the 8 CSVs from JacketData/ — front|back|right|left x L|XL.
public class JacketData
{
    public const int CoordScale = 50;   // Form2: originalX * 50
    public const int MarginX    = 30;
    public const int MarginY    = 30;

    private const string ConfigFile  = "config.txt";
    private const string DefaultDir  = "JacketData";

    private static readonly string[] Views = { "front", "back", "right", "left" };
    private static readonly string[] Sizes = { "L", "XL" };

    private readonly Dictionary<string, List<Circle>> _l  = new Dictionary<string, List<Circle>>();
    private readonly Dictionary<string, List<Circle>> _xl = new Dictionary<string, List<Circle>>();

    public string Folder { get; private set; }
    public bool   Loaded { get; private set; }

    /// Form2.GetCurrentCSVData()
    public List<Circle> Get(string size, string view)
    {
        var dict = (size == "XL") ? _xl : _l;
        List<Circle> list;
        return dict.TryGetValue(view, out list) ? list : new List<Circle>();
    }

    public bool Has(string size, string view)
    {
        var dict = (size == "XL") ? _xl : _l;
        return dict.ContainsKey(view) && dict[view].Count > 0;
    }

    // ── folder resolution: config.txt, else JacketData/ next to the exe ──

    public static string ResolveFolder()
    {
        string cfg = SoundWindow.AppFile(ConfigFile);

        if (File.Exists(cfg))
        {
            try
            {
                string saved = File.ReadAllText(cfg).Trim();
                if (Directory.Exists(saved) && Validate(saved))
                {
                    Logger.Write("jacket", "folder from config.txt: " + saved);
                    return saved;
                }

                Logger.Write("jacket", "config.txt path invalid, ignoring: " + saved);
            }
            catch (Exception ex)
            {
                Logger.Write("jacket", "config.txt unreadable: " + ex.Message);
            }
        }

        return SoundWindow.AppFile(DefaultDir);
    }

    public static void SaveFolder(string folder)
    {
        try { File.WriteAllText(SoundWindow.AppFile(ConfigFile), folder); }
        catch (Exception ex) { Logger.Write("jacket", "could not save config.txt: " + ex.Message); }
    }

    /// Form1.ValidateCSVFiles — all 8 must be present.
    public static bool Validate(string folder)
    {
        foreach (string v in Views)
            foreach (string s in Sizes)
                if (!File.Exists(Path.Combine(folder, v + "_" + s + ".csv")))
                    return false;
        return true;
    }

    public static string[] MissingFiles(string folder)
    {
        var missing = new List<string>();
        foreach (string v in Views)
            foreach (string s in Sizes)
            {
                string name = v + "_" + s + ".csv";
                if (!File.Exists(Path.Combine(folder, name))) missing.Add(name);
            }
        return missing.ToArray();
    }

    // ── loading ─────────────────────────────────────────────────────────

    public static JacketData Load()
    {
        return Load(ResolveFolder());
    }

    public static JacketData Load(string folder)
    {
        var data = new JacketData();
        data.Folder = folder;

        if (!Directory.Exists(folder))
        {
            Logger.Write("jacket", "folder not found: " + folder);
            return data;
        }

        string[] missing = MissingFiles(folder);
        if (missing.Length > 0)
            Logger.Write("jacket", "missing " + missing.Length + " file(s): " + string.Join(", ", missing));

        foreach (string view in Views)
        {
            foreach (string size in Sizes)
            {
                string path = Path.Combine(folder, view + "_" + size + ".csv");
                var circles = ReadCsv(path);
                if (circles.Count == 0) continue;

                if (size == "XL") data._xl[view] = circles;
                else              data._l[view]  = circles;

                Logger.Write("jacket", "loaded " + circles.Count + " chips from " + view + "_" + size + ".csv");
            }
        }

        data.Loaded = (data._l.Count > 0 || data._xl.Count > 0);
        if (!data.Loaded) Logger.Write("jacket", "NO chip data loaded from " + folder);

        return data;
    }

    /// Columns: Number, OriginalX, OriginalY, OriginalZ. Header optional.
    private static List<Circle> ReadCsv(string path)
    {
        var circles = new List<Circle>();

        if (!File.Exists(path))
        {
            Logger.Write("jacket", "file not found: " + path);
            return circles;
        }

        string[] lines;
        try { lines = File.ReadAllLines(path); }
        catch (Exception ex)
        {
            Logger.Write("jacket", "read failed " + path + ": " + ex.Message);
            return circles;
        }

        int start = 0;
        if (lines.Length > 0)
        {
            string h = lines[0].ToLower();
            if (h.Contains("number") || h.Contains("originalx")) start = 1;
        }

        for (int i = start; i < lines.Length; i++)
        {
            string line = lines[i].Trim();
            if (line.Length == 0) continue;

            string[] p = line.Split(',');
            if (p.Length < 4)
            {
                Logger.Write("jacket", path + " line " + (i + 1) + ": only " + p.Length + " columns");
                continue;
            }

            float number, ox, oy, oz;
            var inv = CultureInfo.InvariantCulture;

            // InvariantCulture matters — under ja_JP/de_DE, "1.5" can fail to parse.
            if (!float.TryParse(p[0].Trim(), NumberStyles.Float, inv, out number) ||
                !float.TryParse(p[1].Trim(), NumberStyles.Float, inv, out ox) ||
                !float.TryParse(p[2].Trim(), NumberStyles.Float, inv, out oy) ||
                !float.TryParse(p[3].Trim(), NumberStyles.Float, inv, out oz))
            {
                Logger.Write("jacket", path + " line " + (i + 1) + ": bad number format");
                continue;
            }

            circles.Add(new Circle
            {
                Number    = number,
                X         = (int)(ox * CoordScale) + MarginX,
                Y         = (int)(oy * CoordScale) + MarginY,
                OriginalX = ox,
                OriginalY = oy,
                OriginalZ = oz,
                Inner     = (number % 1 != 0)
            });
        }

        return circles;
    }
}