https://www.pexels.com/photo/close-up-code-coding-computer-239898/

Konfiguracja położenia sensorów cz. 2 (JS)

Cześć! Tak jak zapowiadałem, ograniczyłem częstotliwość publikowania postów, natomiast nawet mnie samego zaskoczył fakt, jak bardzo! Nawet wypadłem z listy postów na stronie konkursu „Daj się poznać 2017”. Niestety ten tydzień obfitował w różne niespodziewane zadania do realizacji, nie zawsze związane z nauką programowania czy prowadzeniem bloga. Niemniej jednak nie był to czas stracony, bo część tych „atrakcji” można zaliczyć do kategorii wyzwań zawodowych. Z innej strony sytuacja po raz kolejny zmusiła mnie do udowodnienia, że jest możliwe zaliczenie dwóch dni roboczych w ciągu jednej doby.

Niestety w tym tygodniu nie znalazłem czasu na rozwój projektu Sterownika Domowego, nie było żadnego commita, ani nawet nie powstała żadna nowa linia kodu. W kwestii blogowania mam do dokończenia opis realizacji zmiany lokalizacji sensorów realizowany w JS, który rozpocząłem w poprzednim wpisie.

Jeszcze przed tygodniem opisałem strukturę części kodu wykorzystywanego w konfiguracji położenia sensora w postaci dwóch JavaScriptowych modułów homeModule.js oraz utilitiesModule.js. Dziś przedstawię trzeci moduł realizujący główną funkcjonalność oraz korzystający z dwóch poprzednich – disposeSensorModule.js.

HomeController.namespace("HomeController.disposeSensor");
HomeController.disposeSensor = (function () {
    let _isPositionNotSelected = true,
            _sensorPositionX = 0,
            _sensorPositionY = 0,
            _canvas = {};
 
    const _home = HomeController.home,
            _utils = HomeController.utilities;
 
    const _btnSave = {x: 810, y: 450, width: 120, height: 30},
            _btnReset = {x: 680, y: 450, width: 120, height: 30},
            _inactiveButtonColor = "rgb(172, 172, 172)",
            _activeButtonColor = "rgb(252, 184, 20)";
 
    $(function () {
        $("[data-hide]").on("click", function () {
            $("." + $(this).attr("data-hide")).hide();
            $('#message').contents().filter(function () {
                return this.nodeType === 3 || $(this).is('br');
            }).remove();
        });
    });
 
    function _selectSensorPosition(evt){
    // (...)
    }
    return {
        init: function () {
            _canvas = _home.init();
            _canvas.addEventListener("click", _selectSensorPosition);
        }
    };
}());

Tak wygląda szkielet modułu, z wyciętym ciałem metody _selectSensorPosition() dla poprawy czytelności. Funkcja ta realizuje obsługę zdarzenia typu click na elemencie <canvas> (dodanie eventListnera w init()). O tym co się dzieje wewnątrz niej, już za chwilę. Warto spojrzeć na budowę tego modułu. Kształtują się w nim wyraźne sekcje. Na początku umieszczone są zmienne modułu zadeklarowane za pomocą słowa kluczowego let, dalej są wykorzystywane zależności zewnętrzne oraz stałe (const), po czym znajdują się metody. Na końcu znajduje się sekcja return z publicznie dostępnymi metodami (póki co tylko z funkcją init()).

Komunikat po zapisaniu pozycji sensora
Komunikat po zapisaniu pozycji sensora

Dla przypomnienia, funkcjonalność edycji miejsca zainstalowania sensora realizowana jest w kilku krokach:

  1. Wybór czujnika z listy sensorów
  2. Wybór lokalizacji czujnika na rzucie mieszkania
  3. Zatwierdzenie wyboru lub wprowadzenie poprawki

Po zatwierdzeniu wyboru użytkownik zobaczy komunikat z informacją o poprawnie wykonanej operacji lub o błędzie, jeśli coś poszło nie tak. Wyświetlenie tej notki zrealizowałem w oparciu o Bootstrap i jego Closing Alerts.

        <div id="message" class="hidden alert-dismissible" role="alert">
            <button type="button" class="close" data-hide="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>

Domyślne zachowanie podczas zamykania nie do końca mi odpowiadało, element w momencie przyciśnięcia x, zostaje po prostu usunięty z drzewa DOM dokumentu HTML. W przypadku, gdy użytkownik zechce wprowadzić poprawkę, nie ma już elementu o identyfikatorze message, więc komunikat się nie wyświetli. Funkcja anonimowa znajdująca się w ciele modułu nadpisuje to domyślne zachowanie, nie kasując elementu, a jedynie ukrywając oraz czyszcząc jego zawartość tekstową i ewentualne znaczniki <br>, które mogą się tam pojawić. Jej implementacja opiera się o jQuery, a najciekawszym fragmentem jest chyba filtrowanie zawartości znacznika i usuwanie tylko wybranych elementów. Operacja ta wykorzystuje funkcję anonimową, która sprawdza m.in. typ węzła. Zapis this.nodeType === 3 nie wydaje mi się zbyt piękny, więc do mojej listy TODO dopisałem konieczność poszukania innych, bardziej eleganckich rozwiązań. Typy węzłów można znaleźć na stronie www.w3schools.com.

Spójrzmy teraz jak wygląda implementacja funkcji wykonywanej w przypadku wystąpienia zdarzenia typu click na elemencie <canvas>. Jej czytanie należy rozpocząć prawie pod sam koniec, od wyrażenia warunkowego if(_isPositionNotSelected).

function _selectSensorPosition(evt) {
 
        // private functions of click event handler
        const   _selectPosition = function () {
        // (...)
        },
                _showActionButtons = function () {
                    _setAllButtonsInactive();
                },
                _setAllButtonsInactive = function () {
                    _utils.showButton(_btnSave, _inactiveButtonColor, "Zapisz");
                    _utils.showButton(_btnReset, _inactiveButtonColor, "Popraw");
                },
                _mouseOverButtonEventFunction = function (evt) {
                // (...)
                    }
                },
                _resetChoice = function () {
                // (...)
                },
                _savePosition = function () {
                    let sensorId = $("#sensorId").text();
                    _ajaxSetSensorPosition(sensorId, _sensorPositionX, _sensorPositionY);
                },
                _ajaxSetSensorPosition = function (sensorId, x, y) {
                // (...)
                },
                _handleSetSensorPositionResponse = function (response) {
                // (...)
                },
                _getColorOfConfiguredSensor = function () {
                // (...)
                },
                _isMouseOverButton = function (mousePosition, btn) {
                    return _utils.isMouseInsideRectangle(mousePosition, btn);
                };
 
        // Main body of click event handler
        if (_isPositionNotSelected) {
            _selectPosition();
        } else {
            const isMouseOverResetButton = _isMouseOverButton(_utils.getMousePos(evt), _btnReset),
                    isMouseOverSaveButton = _isMouseOverButton(_utils.getMousePos(evt), _btnSave);
 
            if (isMouseOverResetButton) {
                _resetChoice();
            } else if (isMouseOverSaveButton) {
                _savePosition();
            }
        }
    }

Działanie funkcji rozpoczyna się od sprawdzenia stanu zmiennej _isPositionNotSelected zapamiętującej czy pozycja została wcześniej wybrana. W zależności od wyniku testu wykonywana jest logika odczytująca współrzędne kursora w momencie zdarzenia, rysująca okrąg symbolizujący czujnik o środku w wybranym punkcie, wyświetlająca przyciski Popraw i Zapisz oraz dodająca obsługę zdarzenia typu mousemove (w przypadku przesunięcia kursora myszy nad obszar przycisku, zmienia on swój kolor). Wszystko powyższe inicjowane jest w funkcji _selectPosition(). Jeśli pozycja została wybrana, kolejne kliknięcia mogą realizować jedynie operacje obsługi przycisków. W zależności od wyboru operatora wykonywana jest funkcja _resetChoice() lub _savePosition().

W implementacji wszystkich metod prywatnych _selectSensorPosition() nie ma żadnej magii wartej szczególnego opisywania. Warto jedynie wspomnieć, że operacja zapisywania pozycji sensora w bazie danych inicjowana jest za pomocą asynchronicznego przesłania danych (AJAX) w metodzie _ajaxSetSensorPosition(). Odpowiedź od serwera analizowana jest w _handleSetSensorPositionResponse(), gdzie w przypadku odczytania wartości „correct” wyświetlany jest komunikat o powodzeniu operacji wraz z odnośnikiem do strony z listą sensorów.

 _handleSetSensorPositionResponse = function (response) {
                    let message = $("#message");
                    message.removeClass("hidden").removeAttr("style");;
                    if (response !== "") {
                        if (response === "correct") {
                            message.addClass("alert alert-success");
                            message.append("Ustawienie lokalizacji sensora przebiegło pomyślnie. Przejdź do ");
                            message.append("<a href=\"/sensors/list\">listy sensorów</a>.<br/>");
                        } else {
                            message.addClass("alert alert-danger");
                            message.append("UWAGA! Ustawienie lokalizacji sensora zakończyło się błędem.<br/>");
                        }
                    }
                }

Od strony serwera wymagane było dodanie nowej metody setSensorPosition() w kontrolerze SensorController.

    public Result setSensorPosition(int sensorId, int x, int y) {
        if (sensorService.setSensorPosition(sensorId, x, y)) {
            return ok("correct");
        } else {
            return ok("incorrect");
        }
    }

Z kontrolera sterowanie przekazywane jest do warstwy serwisu, która korzystając dalej z warstwy danych, aktualizuje położenie czujnika.

    @Override
    public boolean setSensorPosition(int sensorId, int x, int y) {
        try {
            Sensor sensor = getSensorById(sensorId);
            sensor.positionX = x;
            sensor.positionY = y;
            sensorRepository.updateSensor(sensor);
            return true;
        } catch (NoSensorException ex) {
            return false;
        }
    }

Wygląda na to, że udało mi się dotrzymać postanowienia i jednak wypuścić post w tym tygodniu, co mnie niezmiernie cieszy. Już teraz wiem, że po weekendzie czekają mnie inne zadania programistyczne i jak przypuszczam kolejny wpis może pojawić się pod koniec przyszłego weekendu. W ramach dalszych prac związanych ze Sterownikiem Domowych chciałbym przygotować widok wyświetlający interaktywny rzut mieszkania z wszystkimi czujnikami oraz informacjami o nich, przy realizacji którego na pewno wykorzystam ponownie kod napisany w module homeModule.js. Serdecznie zapraszam do śledzenia moich dalszych poczynań!

2 thoughts to “Konfiguracja położenia sensorów cz. 2 (JS)”

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *