Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

Krótkie ale bardzo użyteczne wyrażenie regularne - lookbehind, lazy, group i backreference

Ostatnio chciałem wyciągnąć z logów komunikaty wysyłane do zewnętrznego systemu i przeprowadzić kilka operacji LINQ to XML na pozyskanych danych. Oto przykładowa linia logu (uproszczona, rzeczywisty log był o wiele bardziej skomplikowany ale nie ma to znaczenia w tym poscie):

Call:<getName seqNo="56789"><id>123</id></getName> Result:<getName seqNo="56789">John Smith</getName>

Interesowały mnie takie dane XML ("call"):

<getName seqNo="56789">
  <id>123</id>
</getName>

Przy okazji: najprostszym sposobem na pozyskanie ładnie sformatowanego XMLa w .NET 3.5 lub nowszym jest wywołanie metody ToString na obiekcie XElement:

var xml = System.Xml.Linq.XElement.Parse(someUglyXmlString);     
Console.WriteLine(xml.ToString());

Jeśli chodzi o log, kilka rzeczy było pewnych:

  • XML z callem znajduje się po tekście „Call:” z początku linii
  • nazwa głównego elementu (roota) komunikatu będzie skądać się wyłącznie ze znaków alfanumerycznych lub podkreślenia
  • w komunikacie nie będzie znaków podziału linii
  • głowy element calla może wystąpić w logu także w sekcji „Result”
Otrzymanie prawidłowych danych było dosyć proste dzięki klasie Regex:
Regex regex = new Regex(@"(?<=^Call:)<(\w+).*?</\1>");
string call = regex.Match(logLine).Value;

To krótkie wyrażenie regularne ma kilka interesujących części. Nie jest może doskonałe ale okazało się bardzo przydatne podczas analizy logów. Jeśli powyższy regex nie jest dla Ciebie zupełnie jasny – czytaj dalej, prędzej czy później będziesz musiał użyć czegoś podobnego.

Poniżej jest to samo wyrażenie ale z komentarzami (ustawienie opcji RegexOptions.IgnorePatternWhitespace jest konieczne do przetworzenia wyrażenia zapisanego w ten sposób):

string pattern = @"(?<=^Call:) # Positive lookbehind dla znacznika wywołania
                   <(\w+)      # Capturing group dla nazwy tagu otwierającego
                   .*?         # Lazy wildcard (wszystko w środku)
                   </\1>       # Backreference do nazwy tagu otwierającego";   
Regex regex = new Regex(pattern, RegexOptions.IgnorePatternWhitespace);
string call = regex.Match(logLine).Value;

Positive lookbehind

(?<=Call:) to tzw. lookaround a dokładniej positive lookbehind. Jest to asercja zerowej szerokości, która pozwala na sprawdzanie czy tekst poprzedzony jest przez dany ciąg znaków. Tutaj „Call:” to tekst poprzedzający, którego szukamy. Zapis (?<=something) określa positive lookbehind. Istnieje również negative lookbehind zapisywany za pomocą (?<!something). Dzięki niemu można zweryfikować czy jakiś tekst nie posiada określonych znaków przed sobą. Lookaround sprawdza fragment tekstu ale nie stanowi on części dopasowanej wartości. Tak więc rezultatem tego:

Regex.Match("X123", @"(?<=X)\d*").Value

będzie „123” a nie „X123”

Silnik wyrażeń regularnych w .NET obsługuje także mechanizm lookaheads. Odwiedź  świetną stronę jeśli chcesz dowiedzieć się więcej o lookarounds.

Uwaga: w niektórych przypadkach (np. w naszym badaniu logu) zamiast positive lookaround można użyć grup nieprzechwytujących...

Capturing group

<(\w+) dopasowuje znak mniejszości, po którym następuje jeden lub więcej znaków z klasy \w (litery, cyfry lub znaki podkreślenia). Fragment \w+ jest otoczony nawiasami w celu utworzenia grupy zawierającej nazwę korzenia XML (getName dla przykładowej linii logu). Grupa ta jest później użyta do znalezienia tagu zamykającego przy użyciu odwołania wstecznego. (\w+) to grupa przechwytujaca (capturing group), co oznacza, że rezultat istnienia tej grupy jest dodawany do kolekcji Groups obiektu Match. Jeśli chcesz umieścić część wyrażenia w grupie ale nie chcesz wstawiać rezultatu do kolekcji Groups możesz skorzystać z grupy nieprzechwytującej. Grupę taką tworzy się poprzez dodanie pytajnika i dwukropka przed nawiasem otwierającym: (?:something) 

Lazy wildcard

.*? dopasowuje wszystkie znaki z wyjątkiem nowe linii (ponieważ nie używamy opcji RegexOptions.Singleline) w trybie leniwym (lazy lub non-gready) dzięki pytajnikowi umieszczonemu za gwiazdką. Domyślnie kwantyfikator * działa w trybie zachłannym (greedy) co oznacza, że silnik wyrażeń regularnych próbuje dopasować tak dużo tekstu jak to możliwe. W naszym przypadku, domyślny tryb spowodowałby zwrócenie zbyt długiego tekstu:

<getName seqNo="56789"><id>123</id></getName> Result:<getName seqNo="56789">John Smith</getName>

Backreference

</\1> dopasowuje zamykający tag XML, którego nazwa dostarczona jest przez \1 backreference. Wspomniana wcześniej grupa (\w+) ma numer 1, przez użycie składni \1 odwołujemy się do tekstu dopasowanego przez tą grupę. Więc dla naszego przykładowego logu </\1> zwraca </getName>. Jeśli wyrażenie regularne jest skomplikowane, dobrym pomysłem jest porzucenie numerowanych referencji na rzecz nazwanych. Grupę można nazwać poprzez składnię <name> lub <’name’> a odwołać się do niej można dzięki k<name> lub k’name’. Wyrażenie może więc wyglądać tak:

@"(?<=^Call:)<(?<tag>\w+).*?</\k<tag>>"

lub tak:

@"(?<=^Call:)<(?'tag'\w+).*?</\k'tag'>"

W naszym przypadku wersja druga jest lepsza. Użycie znaków < > w przypadku dopasowywania XML jest mylące. Silnik regex poradzi sobie z zapisem używającym < > ale pamiętaj, że kod źródłowy piszę się dla ludzi...

Wyrażenia regularne wyglądają strasznie ale warto poświęcić kilka godzin na ich przećwiczenie, regexy to niesamowicie przydatne narzędzie (nie tylko w czasie analizy logów)!

Szybkie operacje pikselowe w .NET (z i bez unsafe)

Klasa Bitmap posiada metody GetPixel i SetPixel, które pozwalają na pobranie i zmianę koloru piksela. Metody te są bardzo proste w użyciu, niestety są też niezwykle powolne. Mój poprzedni post szczegółowo opisuje ten temat, kliknij tutaj jeśli chcesz wiedzieć więcej.

Na szczęście nie trzeba używać zewnętrznych bibliotek (lub całkowicie rezygnować z .NET) by efektywnie przetwarzać obraz. Framework ma wbudowaną klasę ColorMatrix pozwalającą na wykonanie zmian na mapie bitowej w krótkim czasie. Właściwości takie jak kontrast lub nasycenie mogą być modyfikowane w ten sposób. Co jednak z manipulowaniem pojedynczymi pikselami? Da się to zrobić za pomocą metody Bitmap.LockBits i klasy BitmapData

Dobrym testem szybkości operacji pikselowych jest badanie różnicy kolorów. Zadanie polega na znalezieniu fragmentów obrazu, które mają kolor podobny do zadanego koloru. Jak sprawdzić czy kolory są podobne? Pomyśl o kolorze jako punkcie w trójwymiarowej przestrzeni, gdzie osiami są: czerwony, zielony i niebieski. Dwa kolory to dwa punkty. Różnica między kolorami jest więc opisana przez dystans dzielący te dwa punkty w przestrzeni RGB.

Kolory jako punkty w przestrzeni 3D

diff = sqrt((C1R-C2R)2+(C1G-C2G)2+(C1B-C2B)2)

Ta technika jest bardzo prosta w realizacji i daje przyzwoite rezultaty. Porównywanie kolorów jest jednak zagadnieniem bardzo złożonym. Inne przestrzenie koloru niż RGB nadają się lepiej do tego zadania. Pod uwagę należy również brać sposób percepcji koloru przez człowieka (np. nasze oczy są bardziej wyczulone na różnice w odcieniach zieleni niż koloru niebieskiego). Trzymajmy się jednak prostego rozwiązania…

Naszym testowym obrazem będzie to zdjęcie Ultra HD 8K: 7680x4320, 33.1Mpx (na potrzeby bloga je oczywiście zmniejszyłem by nie marnować transferu):

Obraz wejściowy dla badania różnicy koloru (zmniejszony dla bloga)

Oto metoda, która może np. wyszukać piksele R=255 G=161 B=71 (numer auta "36") i ustawić je na kolor biały (reszta stanie się czarna):

static void DetectColorWithGetSetPixel(Bitmap image, byte searchedR, byte searchedG, int searchedB, int tolerance)
{
    int toleranceSquared = tolerance * tolerance;

    for (int x = 0; x < image.Width; x++)
    {
        for (int y = 0; y < image.Height; y++)
        {
            Color pixel = image.GetPixel(x, y);

            int diffR = pixel.R - searchedR;
            int diffG = pixel.G - searchedG;
            int diffB = pixel.B - searchedB;

            int distance = diffR * diffR + diffG * diffG + diffB * diffB;

            image.SetPixel(x, y, distance > toleranceSquared ? Color.Black : Color.White);
        }
    }
}

Powyższy kod to nasza okropnie wolna baza porównawcza Get/SetPixel. Przy wywołaniu w taki sposób (parametry nazwana dla czytelności):

DetectColorWithGetSetPixel(image, searchedR: 255, searchedG: 161, searchedB: 71, tolerance: 60);

otrzymamy taki oto wynik:

Obraz wyjściowy dla badania różnicy koloru (zmniejszony dla bloga)

Rezultat jest całkiem ok ale czekanie na niego ponad 84300ms* to całkowita porażka.

Teraz rzuć okiem na tą metodę:

static unsafe void DetectColorWithUnsafe(Bitmap image, byte searchedR, byte searchedG, int searchedB, int tolerance)
{
    BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    int bytesPerPixel = 3;

    byte* scan0 = (byte*)imageData.Scan0.ToPointer();
    int stride = imageData.Stride;

    byte unmatchingValue = 0;
    byte matchingValue = 255;
    int toleranceSquared = tolerance * tolerance;

    for (int y = 0; y < imageData.Height; y++)
    {
        byte* row = scan0 + (y * stride);

        for (int x = 0; x < imageData.Width; x++)
        {
            // Uwaga na rzeczywistą kolejność (BGR)!
            int bIndex = x * bytesPerPixel;
            int gIndex = bIndex + 1;
            int rIndex = bIndex + 2;

            byte pixelR = row[rIndex];
            byte pixelG = row[gIndex];
            byte pixelB = row[bIndex];

            int diffR = pixelR - searchedR;
            int diffG = pixelG - searchedG;
            int diffB = pixelB - searchedB;

            int distance = diffR * diffR + diffG * diffG + diffB * diffB;

            row[rIndex] = row[bIndex] = row[gIndex] = distance > toleranceSquared ? unmatchingValue : matchingValue;
        }
    }

    image.UnlockBits(imageData);
}

Robi dokładnie to samo ale wykonuje się tylko 230ms ponad 360 razy szybciej!

Kod ten używa metody Bitmap.LockBits, która jest oprawką na natywną funkcję GdipBitmapLockBits (GDI+, gdiplus.dll). LockBits tworzy tymczasowy bufor przechowujący informacje o pikselach w żądanym formacie (w naszym przypadku RGB, po 8 bitów na składową koloru). Wszelkie zmiany w tym buforze są kopiowane do bitmapy przy wywołaniu UnlockBits (dlatego zawsze należy używać LockBits i UnlockBits jako pary). Bitmap.LockBits zwraca obiekt BitmapData (namespace System.Drawing.Imaging), który posiada dwie interesujące właściwości: Scan0 i Stride. Scan0 zwraca adres danych pierwszego piksela. Stride to szerokość jednego rzędu pikseli (tzw. scan line) w bajtach (z opcjonalnym wypełnieniem tak by wartość była podzielna przez 4). 

Układ BitmapData

Zauważ, że nie używam odwołań do Math.Pow i Math.Sqrt by obliczyć dystans między kolorami. Pisanie takiego kodu:

 

double distance = Math.Sqrt(Math.Pow(pixelR - searchedR, 2) + Math.Pow(pixelG - searchedG, 2) + Math.Pow(pixelB - searchedB, 2));

do przetwarzania milionów pikseli do straszny pomysł. Taka linia uczyniła by naszą zoptymalizowaną metodę około 25 razy wolniejszą. Używanie Math.Pow z parametrami całkowitymi to czyste marnotrawstwo, nie trzeba też wyliczać pierwiastka kwadratowego by określić czy odległość mieści się w pewnej tolerancji.

Pokazana wcześniej metoda jest oznaczona słowem kluczowym unsafe. Pozwala ono programowi C# na użycie operacji wskaźnikowych. Niestety tryb unsafe ma istotne ograniczenia. Kod, który go używa musi być skompilowany z opcją \unsafe oraz musi być wykonywany w pełni zaufanym assembly.

Na szczęście istnieje metoda Marshal.Copy (z przestrzeni nazw System.Runtime.InteropServices), dzięki której można przenosić dane między zarządzaną a niezarządzana pamięcią. Możemy jej użyć do skopiowania danych obrazu do tablicy bajtów i manipulowania pikselami w bardzo efektywny sposób. Przeanalizuj tą metodę:

static void DetectColorWithMarshal(Bitmap image, byte searchedR, byte searchedG, int searchedB, int tolerance)
{        
    BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    byte[] imageBytes = new byte[Math.Abs(imageData.Stride) * image.Height];
    IntPtr scan0 = imageData.Scan0;

    Marshal.Copy(scan0, imageBytes, 0, imageBytes.Length);
  
    byte unmatchingValue = 0;
    byte matchingValue = 255;
    int toleranceSquared = tolerance * tolerance;

    for (int i = 0; i < imageBytes.Length; i += 3)
    {
        byte pixelB = imageBytes[i];
        byte pixelR = imageBytes[i + 2];
        byte pixelG = imageBytes[i + 1];

        int diffR = pixelR - searchedR;
        int diffG = pixelG - searchedG;
        int diffB = pixelB - searchedB;

        int distance = diffR * diffR + diffG * diffG + diffB * diffB;

        imageBytes[i] = imageBytes[i + 1] = imageBytes[i + 2] = distance > toleranceSquared ? unmatchingValue : matchingValue;
    }

    Marshal.Copy(imageBytes, 0, scan0, imageBytes.Length);

    image.UnlockBits(imageData);
}

Wykonuje się ona tylko 280ms, jest wiec jedynie odrobinę wolniejsza od wersji unsafe. Pod względem CPU kod jest oszczędny jednak zużywa więcej pamięci niż poprzednia metoda – prawie 100 megabajtów dla naszego testowego obrazu Ultra HD 8K w formacie RGB 24.

Jeśli chcesz uzyskać jeszcze większą prędkość obróbki obrazu możesz przetwarzać różne części obrazu równolegle. Musisz jednak wykonać nieco testów ponieważ dla małych obrazów koszt użycia wielu wątków może okazać się większy niż zysk z wykonywania równoległego. Poniższa metoda to przykład kodu, który używa 4 wątków do jednoczesnego procesowania 4 fragmentów obrazu. Na mojej maszynie daje to ok 30% wzrostu wydajności. Traktuj ten kod jako szybką wskazówkę, ten post jest już i tak zbyt długi…

static unsafe void DetectColorWithUnsafeParallel(Bitmap image, byte searchedR, byte searchedG, int searchedB, int tolerance)
{
    BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    int bytesPerPixel = 3;

    byte* scan0 = (byte*)imageData.Scan0.ToPointer();
    int stride = imageData.Stride;

    byte unmatchingValue = 0;
    byte matchingValue = 255;
    int toleranceSquared = tolerance * tolerance;

    Task[] tasks = new Task[4];
    for (int i = 0; i < tasks.Length; i++)
    {
        int ii = i;
        tasks[i] = Task.Factory.StartNew(() =>
            {
                int minY = ii < 2 ? 0 : imageData.Height / 2;
                int maxY = ii < 2 ? imageData.Height / 2 : imageData.Height;

                int minX = ii % 2 == 0 ? 0 : imageData.Width / 2;
                int maxX = ii % 2 == 0 ? imageData.Width / 2 : imageData.Width;                        
                
                for (int y = minY; y < maxY; y++)
                {
                    byte* row = scan0 + (y * stride);

                    for (int x = minX; x < maxX; x++)
                    {
                        int bIndex = x * bytesPerPixel;
                        int gIndex = bIndex + 1;
                        int rIndex = bIndex + 2;

                        byte pixelR = row[rIndex];
                        byte pixelG = row[gIndex];
                        byte pixelB = row[bIndex];

                        int diffR = pixelR - searchedR;
                        int diffG = pixelG - searchedG;
                        int diffB = pixelB - searchedB;

                        int distance = diffR * diffR + diffG * diffG + diffB * diffB;

                        row[rIndex] = row[bIndex] = row[gIndex] = distance > toleranceSquared ? unmatchingValue : matchingValue;
                    }
                }
            });
    }

    Task.WaitAll(tasks);

    image.UnlockBits(imageData);
}

Aktualizacja 2018-01-08: Jeśli zależy Ci na naprawdę wydajnym przetwarzaniu obrazu powinieneś sięgnąć po wyspecjalizowaną bibliotekę, taką jak OpenCV. Kilka miesięcy temu napisałem serie postów "Detecting a Drone - OpenCV in .NET for Beginners (Emgu CV 3.2, Visual Studio 2017)", które pomogą Ci zacząć...

* Aplikacja konsolowa .NET 4 uruchamiana na laptopie MSI GE620 DX: Intel Core i5-2430M 2.40GHz (2 rdzenie, 4 wątki), 4GB DDR3 RAM, NVIDIA GT 555M 2GB DDR3, HDD 500GB 7200RPM, Windows 7 Home Premium x64.

Radio buttony dla elementów listy w MVC 4 – problem z unikalnością id

Załóżmy, że mamy pewien model, który posiada listę i chcemy wytworzyć radio buttony dla elementów tej listy. Rzuć okiem na poniższy przykład.

Główna klasa modelu zawierająca listę:

using System.Collections.Generic;

public class Team
{
    public string Name { get; set; }
    public List<Player> Players { get; set; }
}

Klasa dla elementów listy:

public class Player
{
    public string Name { get; set; }
    public string Level { get; set; }
}

Istnieją trzy akceptowalne wartości dla właściwości Level:  BEG (Beginner), INT (Intermediate) oraz ADV (Advanced), chcemy więc wyświetlić trzy radio buttony (wraz z etykietami) dla każdego z zawodników w drużynie. Normalnie pewnie użylibyśmy enum dla property Level ale w imię prostoty przykładu zostaniemy przy typie string...

Metoda kontrolera zwracająca przykładowe dane:

public ActionResult Index()
{
    var team = new Team() {
        Name = "Some Team",
        Players = new List<Player> {
               new Player() {Name = "Player A", Level="BEG"},
               new Player() {Name = "Player B", Level="INT"},
               new Player() {Name = "Player C", Level="ADV"}
        }
    };

    return View(team);
}

Oto widok Index.cshtml:

@model Team

<section>
    <h1>@Model.Name</h1>        

    @Html.EditorFor(model => model.Players)            
</section>

Zauważ, że markup dla właściwości Players nie jest tworzony wewnątrz pętli. Zamiast tego użyty jest EditorTemplate. Jest to zalecane podejście ponieważ sprawia, że kod jest bardziej przejrzysty i prostszy w utrzymaniu. Framwork jest na tyle sprtytny, że używa kodu z szablonu edytora dla każdego z zawodników na liście ponieważ Team.Players implementuje interfejs IEnumerable.

A oto wspomniany Players.cshtml EditorTemplate:

@model Player
<div>
    <strong>@Model.Name:</strong>

    @Html.RadioButtonFor(model => model.Level, "BEG")
    @Html.LabelFor(model => model.Level, "Beginner")

    @Html.RadioButtonFor(model => model.Level, "INT")
    @Html.LabelFor(model => model.Level, "Intermediate")

    @Html.RadioButtonFor(model => model.Level, "ADV")
    @Html.LabelFor(model => model.Level, "Advanced")        
</div>

Kod wygląda w porządku, używa silnie typowanych helperów opartych o wyrażenia lambda (dla wychwycenia błędów podczas kompilacji i łatwiejszego refaktoringu)... niestety, jest pewien problem: HTML wygenerowany przez taki kod ma bardzo poważną wadę. Sprawdź poniższy fragment kodu źródłowego strony, który został wygenerowany dla pierwszego zawodnika z listy:

<div>
    <strong>Player A:</strong>  
 
    <input name="Players[0].Level" id="Players_0__Level" type="radio" checked="checked" value="BEG">
    <label for="Players_0__Level">Beginner</label>
 
    <input name="Players[0].Level" id="Players_0__Level" type="radio" value="INT">
    <label for="Players_0__Level">Intermediate</label>
 
    <input name="Players[0].Level" id="Players_0__Level" type="radio" value="ADV">
    <label for="Players_0__Level">Advanced</label>        
</div>

Identyfikator Players_0__Level jest użyty dla trzech różnych radio buttonów! Brak unikalności nie tylko narusza specyfikacje HTML i utrudnia oskryptowanie, powoduje też, że etykiety (tagi label) nie działają prawidłowo (tj. kliknięcie nie powoduje zaznaczenia odpowiedniego elementu input). 

Na szczęście biblioteka MVC posiada klasę TemplateInfo z metodą GetFullHtmlFieldId. Metoda ta zwraca identyfikator dla elementu DOM. Identyfikator jest budowany poprzez połączenie argumentu przekazanego do metody z automatycznie wygenerowanym prefiksem. Prefiks uwzględnia poziom zagnieżdżenia i indeks elementu listy. Wewnętrznie, GetFullHtmlFieldId odwołuje się do właściwości TemplateInfo.HtmlFieldPrefix i metody TagBuilder.CreateSanitizedId więc w przypadku przekazania do sufiksu znaków niemogących być częścią id, znaki te zostaną usunięte.

Oto zmodyfikowany kod EditorTemplate:
@model Player
<div>
    <strong>@Model.Name:</strong>

    @{string rbBeginnerId = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("rbBeginner"); }
    @Html.RadioButtonFor(model => model.Level, "BEG", new { id = rbBeginnerId })
    @Html.LabelFor(model => model.Level, "Beginner",  new { @for = rbBeginnerId} )

    @{string rbIntermediateId = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("rbIntermediate"); }
    @Html.RadioButtonFor(model => model.Level, "INT", new { id = rbIntermediateId })
    @Html.LabelFor(model => model.Level, "Intermediate",  new { @for = rbIntermediateId })

    @{string rbAdvancedId = ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId("rbAdvanced"); }
    @Html.RadioButtonFor(model => model.Level, "ADV", new { id = rbAdvancedId })
    @Html.LabelFor(model => model.Level, "Advanced",  new { @for = rbAdvancedId })
</div>

Wywołania metody ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId pozwalają na pozyskanie id dla radio buttonów, identyfikatory te są wykorzystane również do ustawienia atrybutów for elementów label. W MVC 3 nie było przeładowanej metody LabelFor, która akceptowałaby obiekt dla htmlAttributes. Na szczęście wersja 4 ma wbudowany stosowny overload.

Powyższy kod tworzy taki markup:

<div>
    <strong>Player A:</strong>
 
    <input name="Players[0].Level" id="Players_0__rbBeginner" type="radio" checked="checked" value="BEG">
    <label for="Players_0__rbBeginner">Beginner</label>
 
    <input name="Players[0].Level" id="Players_0__rbIntermediate" type="radio" value="INT">
    <label for="Players_0__rbIntermediate">Intermediate</label>
 
    <input name="Players[0].Level" id="Players_0__rbAdvanced" type="radio" value="ADV">
    <label for="Players_0__rbAdvanced">Advanced</label>
</div>

Teraz inputy mają unikalne identyfikatory a labele poprawnie odwołują się do radio buttonów poprzez atrybut for. I w porządku :)

Przy okazji: dziwna nazwa „radio button” pochodzi od odbiorników radiowych, które posiadały panele z przyciskami do wyboru stacji (wciśnięcie jednego guzika automatycznie wybijało pozostałe).

Układ współrzędnych HTML5 Canvas, rysowanie z wartością y rosnącą ku górze ekranu

Układ współrzędnych w HTML 5 Canvas jest ustalony w taki sposób, że za punkt początkowy (0, 0) przyjęty jest lewy-górny róg canvas. To rozwiązanie nie jest niczym niezwykłym w świecie grafiki ekranowej (tak samo jest np. w Windows Forms czy SVG). Popularne kiedyś monitory CRT wyświetlały linie obrazu w kolejności od góry do dołu a obraz w linii tworzony był od lewej do prawej. Umieszczenie punktu (0,0) w lewym-górnym rogu było więc intuicyjne i ułatwiało budowanie sprzętu i oprogramowania do obsługi grafiki… 

Niestety czasem domyślny układ współrzędnych na canvas jest mało praktyczny. Przyjmijmy, że chcesz wykonać animację lotu pocisku. Naturalnie wydaje się, że dla wznoszącego się pocisku wartość współrzędnej y powinna rosnąć. Da to jednak dziwaczny efekt odwróconej trajektorii:

Domyślny układ współrzędnych (y rośnie ku dołowi ekranu)

Można temu zaradzić poprzez modyfikację wartości y przekazywanej do funkcji rysującej:

context.fillRect(x, offsetY - y, size, size);

Dla y = 0, pocisk zostanie umieszczony w miejscu wyznaczonym przez offsetY (by y = 0 oznaczało sam spód canvas ustaw offset na wartość równą wysokości canvas). Im większa będzie wartość y tym wyżej pocisk zostanie narysowany. Problem w tym, że możesz mieć w kodzie setki miejsc, w których wykorzystywana będzie współrzędna y. Wystarczy, że raz zapomnisz uwzględnić offsetY i cały obraz może zostać uszkodzony.

Na szczęście canvas umożliwia wprowadzenie zmian w układzie współrzędnych za pomocą transformacji. Nam przydadzą się dwie metody transformujące: translate(x, y) i scale(x, y). Pierwsza z nich umożliwia przesunięcie początku układu współrzędnych. Druga służy do zmiany wielkości rysowanych obiektów, jednak może też zostać użyta do odwrócenia współrzędnych.

Pojedyncze wykonanie tego kodu sprawi, że początek układu współrzędnych znajdzie się w punkcie (0, offsetY) a wartość y będzie wyższa u góry ekranu:

context.translate(0, offsetY);
context.scale(1, -1);

Przesunięcie i skalowanie układu współrzędnych

Od tej pory możemy wywoływać metody rysujące bez konieczności modyfikacji współrzędnych!

Jest jednak pewien problem: podanie -1 jako wartości drugiego parametru metody scale powoduje, że cały obraz tworzony jest dla odwróconej współrzędnej y. Dotyczy to także tekstu (wywołanie fillText sprawi, że tekst pojawi się „do góry nogami”). Przed wypisaniem tekstu należy więc przywrócić domyślny układ osi y. Ponieważ ręczne przywracanie stanu canvas byłoby bardzo kłopotliwe, istnieją metody save() i restore(), które umożliwiają odpowiednio: odłożenie stanu na stosie i przywrócenie stanu ze stosu. Zaleca się użycie metody save przed dokonaniem transformacji. W stan canvas wchodzą nie tylko użyte transformacje ale też wartości takie jak styl wypełnienia czy grubość linii...

context.save();

context.fillStyle = 'red';
context.scale(2, 2);
context.fillRect(0, 0, 10, 10);

context.restore();

context.fillRect(0, 0, 10, 10);

Powyższy kod sprawi, że zostaną narysowane 2 kwadraty:

Pierwszy z nich ma kolor czerwony i narysowany jest w skali 2x. Drugi jest rysowany w ustawieniach domyślnych canvas (kolor czarny i skala 1x). Dzieje się tak dlatego, że przed zmianami skali i koloru, stan canvas został odłożony na stosie po czym został przywrócony przed narysowaniem drugiego kwadratu.

TortoiseSVN pre-commit hook w C# – uchroń się przed głupim błędem

Chyba każdemu podczas tworzenia albo debugowania programu zdarza się wprowadzać do kodu tymczasowe zmiany, które ułatwiają bieżące zadanie ale nie powinny trafić do repozytorium. Chyba każdemu zdarzyło się też wbić taki kod do kolejnej rewizji. Jeśli masz szczęście błąd szybko się ujawni i skończy się na odrobinie wstydu, jeśli nie...

Gdyby tylko można było jakoś oznaczyć „nie-commitowalny” kod...

Można i to całkiem prosto! 

TortoiseSVN umożliwia ustawienie tzw. pre-commit hook. Jest to program (lub skrypt) uruchamiany w chwili gdy użytkownik naciśnie przycisk „OK”w oknie „SVN Commit”. Hook może np. zbadać zawartość modyfikowanych plików i zablokować commit jeśli zajdzie taka potrzeba. Hooki Tortoise’a różnią się tym od hooków Subversion, że są odpalane lokalnie a nie na serwerze, na którym przechowywane jest repozytorium. Działasz więc na swojej maszynie, nie musisz obawiać się oto czy Twój hook zostanie zaakceptowany przez admina i czy zadziała na serwerze (serwer może np. nie obsługiwać .NET), nie wpływasz też na innych użytkowników repozytorium. Poza tym hooki client-side działają szybciej...

Szczegółowy opis hooków znajduje się w rozdziale „4.30.8. Client Side Hook Scripts” pliku pomocy Tortoise.

TSVN obsługuje 7 rodzai hooków: start-commit, pre-commit, post-commit, start-update, pre-update, post-update oraz pre-connect.  My zajmiemy się obsługą akcji pre-commit. Istotą hooka będzie wykrycie czy wśród dodawanych lub aktualizowanych plików nie znajduje się taki, który zawiera znacznik kodu tymczasowego. Za taki znacznik uznamy tekst „NOT_FOR_REPO”, który można umieścić w komentarzu nad kodem tymczasowym. 

Oto cały kod hooka – prosta aplikacja konsolowa, która może uratować Ci dupę :)

using System;
using System.IO;
using System.Text.RegularExpressions;

namespace NotForRepoPreCommitHook
{
    class Program
    {
        const string NotForRepoMarker = "NOT_FOR_REPO";

        static void Main(string[] args)
        {              
            string[] affectedPaths = File.ReadAllLines(args[0]);

            Regex fileExtensionPattern = new Regex(@"^.*\.(cs|js|xml|config)$", RegexOptions.IgnoreCase);

            foreach (string path in affectedPaths)
            {
                if (fileExtensionPattern.IsMatch(path) && File.Exists(path))
                {
                    if (ContainsNotForRepoMarker(path))
                    {
                        string errorMessage = string.Format("{0} marker found in {1}", NotForRepoMarker, path);
                        Console.Error.WriteLine(errorMessage);    
                        Environment.Exit(1);  
                    }
                }
            }             
        }

        static bool ContainsNotForRepoMarker(string path)
        {
            StreamReader reader = File.OpenText(path);

            try
            {
                string line = reader.ReadLine();

                while (line != null)
                {
                    if (line.Contains(NotForRepoMarker))
                    {
                        return true;
                    }

                    line = reader.ReadLine();
                }
            }
            finally
            {
                reader.Close();
            }  

            return false;
        }
    }
}

TSVN wywołuje hook typu pre-commit z czterema parametrami. Nas interesuje jednak tylko pierwszy parametr, który zawiera ścieżkę do pliku *.tmp. W pliku tym znajdują się ścieżki do obsługiwanych przy danym commicie plików. Każda linia to jedna ścieżka. Po załadowaniu listy plików, filtrujemy je po rozszerzeniu (przydatne jeśli nie chcesz sprawdzać plików wszystkich typów). Ważne jest też sprawdzenie czy plik istnieje – lista w pliku *.tmp  zawiera także ścieżki do usuwanych plików! Samo wykrycie znacznika określonego przez stałą NotForRepoMarker znajduje się w metodzie ContainsNotForRepoMarker. Mimo swojej prostoty zapewnia niezłą wydajność. Na moim (średniej klasy) laptopie, przeglądniecie 100 MB pliku nie trwa nawet sekundy. Jeśli znacznik zostaje wykryty program przerwa pracę z kodem błędu (wartość różna od 0). Przed zakończeniem do standardowego wyjścia błędu (Console.Error) dodawane jest info o tym, który plik posiada znacznik kodu tymczasowego. Tekst ten trafi do okna Tortoise.

Prawa, że kod jest prosty? Na dodatek instalacja hooka jest banalna!

By podłączyć hook wybierz opcje „Settings” z menu kontekstowego TortoiseSVN. Następnie wybierz element „Hook Scripts” i naciśnij przycisk „Add...”. Pojawi się takie okno:

Okno konfiguracji hooków TSVN

Hook Type” ustaw na „Pre-Commit Hook”. W polu „Working Copy Path” ustaw ścieżkę do katalogu z lokalną kopią repo, którego ma dotyczyć hook (różne foldery mogą mieć różne hooki). W polu „Command Line To Execute” ustaw ścieżkę do aplikacji implementującej hooka. Zaznacz opcje „Wait for the script to finish” i „Hide the script while running” (nie chcesz przecież by okno konsoli było na chwilę widoczne). Naciśnij „OK” i voila, hook zainstalowany!

Spróbuj teraz oznaczyć jakiś fragment kodu komentarzem „NOT_FOR_REPO” i wykonać commit. Powinieneś zobaczyć coś takiego:

Zablokowanie operacji przez pre-commit hook

Zwróć uwagę na przycisk „Retry without hooks” – pozwala on na wykonanie operacji commit przy zignorowaniu hooków.

Mamy już hook chroniący przed wgraniem testowego kodu. Można by też pokusić się o hook, który wymusza wpisanie komentarza, blokuje wgranie pliku typu *.log itp. Twoje prywatne hooki – Ty decydujesz! A jeśli któreś z nich przydadzą się całemu zespołowi można zawsze przerobić je na hooki Subversion działające na serwerze.

Testowane na TortoiseSVN 1.7.8/Subversion 1.7.6.1

Aktualizacja 24.03.2014: Wyróżnienie informacji o opcji "Wait for the script to finish" bez niej hook nie zablokuje commita!

Aktualizacja 17.09.2013 (dodatkowe info): Hook może zostać ustawiony na folderze nadrzędnym zawierającym checkouty różnych repozytoriów. Jeśli jesteś gotów poświecić nieco wydajności dla dodatkowego bezpieczeństwa to możesz zrezygnować z filtrowania plików przed sprawdzeniem znacznika NotForRepoMarker.