Anna Tomka Web Developer

Batman vs Superman

Goal

There was a previous post about my demo site Batman vs Superman. It’s a simple voting application, what I created for a Google IO Extended demonstration. It was made with AngularJS 2 in march, but the API changed a lot (really lot) since then so I started to update my demo too. Meanwhile I’ve got some ideas too to improve the main features a little bit. I’m planning to create a step-by-step tutorial that will describe how to build this application on your own. I also have other ideas which are currently under development with some great developers :) I will post about them as soon as they reach a beta version. Until then checkout the actual state of the Batman vs Superman site.

AngularJS Chat

Goal

This is an example that shows how to create a simple Chat application with the following technologies: AngularJS, Angular Material, Socket IO. The full example is available on branch master, but you can start from nearly scratch when you checkout branch step_registration.

Main features:

  • Login
  • Registration
  • Logout
  • View/Update Profile
  • Create room
  • List rooms
  • Open room
  • List users in a room
  • List messages in a room
  • Create new message

Source

Git

If you use Git, you can clone the repository of the application with the following command:

git clone https://github.com/annatomka/angular-chat.git

Download as zip

If you don’t want / can’t use Git, you can download the source from the following url:

https://github.com/annatomka/angular-chat/archive/step_registration.zip

Registration

We have a login form as a starting form, but unfortunately there’s no user we could log in to the chat. For this purpose let’s create the registration form in this section. Extend index.routes.js with the following route:


.state('registration', {
url: '/registration',
templateUrl: 'app/registration/registration.html',
controller: 'RegistrationController',
controllerAs: 'registrationCtrl'
})

As you can see from the code above we need a template and a controller too… Create folder registration and place registration.controller.js here with the following content:


(function () {
'use strict';

angular
  .module('angularChat')
  .controller('RegistrationController', RegistrationController);

/** @ngInject */
function RegistrationController($rootScope, $scope, UserService, User, AccountService, $state) {
  var registrationCtrl = this;
  registrationCtrl.user = new User();

  registrationCtrl.register = function () {
    UserService.create(registrationCtrl.user).then(function (user) {
      $rootScope.toast("Registration successful! You are now logged in :)");
      AccountService.login(user.username, user.password).then(function(){
        $state.go("rooms",{},{reload: true});
      });
    }, function (error) {

    });
  }
}
})();

Add registration.html with the following HTML:


<div class="registration-frame" ui-view="" flex="" layout="row">
<div layout="row" flex="" layout-padding="" layout-fill="" layout-align="center center" class="ng-scope">
  <div flex="40" flex-lg="50" flex-md="70" flex-sm="100">
    <md-card class="md-cyan-theme">
      <md-toolbar class="md-cyan-theme" layout-padding>
        <div layout="column" layout-align="center" class="padding-20 text-center">
          <img ng-src="img/logo.png">
          <h1 layout="row" layout-align="center center" class="md-headline ng-scope" translate="">Registration</h1>
        </div>
      </md-toolbar>
      <md-content class="md-padding md-cyan-theme">
        <form name="registration">
          <basic-info user="registrationCtrl.user"></basic-info>
          <div class="md-actions" layout="column" layout-align="center">
            <md-button class="md-raised md-primary" layout="row" layout-align="center center" ng-disabled="registration.$invalid" ng-click="registrationCtrl.register()">
              Registration
            </md-button>
            <a
              class="md-primary md-button md-cyan-theme" ui-sref="login"
              aria-label="Already registered? Login now">Already registered? Login now</a>
          </div>

        </form>
      </md-content>
    </md-card>
  </div>
</div>
</div>

We need to link somehow the registration form with the login form, so add a registration button to login.html as the following example shows:


<a
class="md-primary md-button md-cyan-theme" ui-sref="registration"
aria-label="Don't have an account? Create one now">Don't have an account? Create one now</a>

Profile

step_profile

If you’ve got stucked in the previous step, just switch to branch step_profile and continue work from here:


git checkout step_profile

Complete the profile.directive.js directive to load the current user:


var loggedinUser = AccountService.getLoggedInUser();
profileDialogCtrl.user = {};

angular.copy(loggedinUser,profileDialogCtrl.user);

We also want to save the profile details, so add the following function too:


profileDialogCtrl.save = function () {

UserService.update(profileDialogCtrl.user).$promise.then(
function(result){
angular.copy(result,loggedinUser);
},
function(error){
	console.log(error);
}
);
$mdDialog.hide();
};

Extend the template (profile.dialog.html) to load the basic form (between md-dialog-content tags):


<basic-info user="profileDialogCtrl.user"></basic-info>

In the same template call the function that saves the profile when the user clicks the button:


ng-click="profileDialogCtrl.save()"

Let’s upgrade our sidebar to a whole new level. To do this, inject AccountService in the leftnav.directive.js and retrieve the current user:


leftnavCtrl.user = AccountService.getLoggedInUser();

Let’s create a button on the sidenav to reach user profile (leftnav.html):


<md-button profile-button class="md-accent" layout="row">
My Profile
</md-button>

We should also display the loggedin user, so paste the following code between the first md-content tags also in the leftnav.html template:


<img class="avatar" ng-src=""/>
<h3 class="fullname" layout="row" layout-align="center center"></h3>
<small class="username" layout="row" layout-align="center center">@</small>

Let’s put the image of the user in the menu. To do this first inject AccountService in the menu.directive.js and query the loggedin user:


menuCtrl.user = AccountService.getLoggedInUser();

Display the user avatar as a button (menu.html), that way the user can click on it and edit its profile:


<md-button profile-button class="md-icon-button" aria-label="More"  ng-if="$root.loggedIn">
<img ng-src="" class="md-avatar message-avatar" style="    height: 50px;
background-color: white;"/>
</md-button>

Szoba létrehozása

step_create_room

Szobákról még nem volt szó, elsőként a modellt alkossuk meg: room.model.js a room mappában.


(function () {
'use strict';

angular
  .module('angularChat')
  .factory('Room', Room);

/** @ngInject */
function Room(apiUrl,$resource) {
  return $resource(apiUrl + '/rooms/:id');
}
})();

A room-ban lehetnek user-ek is, így ennek a modelljét se hagyjuk ki! room.users.model.js:


(function () {
'use strict';

angular
  .module('angularChat')
  .factory('RoomUser', RoomUser);

/** @ngInject */
function RoomUser(apiUrl,$resource) {
  return $resource(apiUrl + '/rooms/:id/users/:userId', { id: '@_id' });
}
})();

Következik a room service. Hozzuk létre a room.service.js fájlt a következő tartalommal:


(function () {
'use strict';

angular
  .module('angularChat')
  .service('RoomService', RoomService);

/** @ngInject */
function RoomService($resource, apiUrl,Room, RoomUser) {
    this.createRoom = createRoom;
 
  function createRoom(room){
    var newRoom = new Room({name : room.name});
    return newRoom.$save();
  }
  }
})();

A szobát létrehozó gomb direktívája a create.room.fab.directive.js-ben található. Innen hiányzik a click eseményt kezelő függvény. Egészítsük ki vele a direktívát:


createRoomCtrl.create = function(){
RoomService.createRoom(createRoomCtrl.newRoom).then(function(result){
  $rootScope.toast("Room "+result._id+" created successfully!");

  $mdDialog.hide();
},function(result){
  console.error(result);
  $rootScope.toast("We couldn't create your room, sorry :(")
});
}

template: create.room.fab.tmpl.html egészítsük ki az eseménykezelővel:


ng-click="fabCtrl.createRoomDialog()"

adjunk egy tooltipet is neki:


<md-tooltip md-direction="left">Create Room</md-tooltip>

create.room.dialog.tmpl.html-ben:


<md-dialog aria-label="New Room">
<form name="createRoomForm">
  <md-toolbar>
    <div class="md-toolbar-tools">
      <h2>Create Room</h2>
      <span flex></span>
      <md-button class="md-icon-button" ng-click="createRoomCtrl.cancel()">
        <md-icon md-svg-src="img/icons/ic_close_24px.svg" aria-label="Close dialog"></md-icon>
      </md-button>
    </div>
  </md-toolbar>
  <md-dialog-content>
     <md-content layout-padding>
      <div layout layout-sm="column">
        <md-input-container flex>
          <label>Room name</label>
          <input ng-enter="createRoomCtrl.create()" ng-model="createRoomCtrl.newRoom.name" required>
        </md-input-container>
      </div>
    </md-content>
  </md-dialog-content>
  <div class="md-actions" layout="column" layout-align="center">
    <md-button class="md-primary md-raised" layout="row" layout-align="center center" ng-click="createRoomCtrl.create()">
      Create
    </md-button>
  </div>
</form>
</md-dialog>

Szobák listázása

step_list_rooms

Ebben a szakaszban létrehozunk egy szobákat listázó panelt, amely a menüben lévő gombra kattintva kúszik fel az oldal alján. Lássuk!

Egészítsük ki a RoomService-t: * az összes szoba lekérdezésére:


this.getRooms = getRooms;
function getRooms() {
    var rooms = Room.query();
    return rooms;
  }
  • szoba felhasználóinak lekérdezése:

this.getUsers = getUsers;

function getUsers(roomId) {
return RoomUser.query({id: roomId});
}

Hozzunk létre a menüben egy gombot, amire felnyílik majd a szobalista: menu/menu.html


<md-button room-list-opener  ng-if="$root.loggedIn">
Rooms
</md-button>

Ez egy sima angularmaterial-os gomb, kiegészítve egy direktívával, amit még nem írtunk meg, így tegyük meg azt is.

room/room.list.directive.js létrehozása:


(function() {
'use strict';

angular
  .module('angularChat')
  .directive('roomListOpener', RoomListOpener);

/** @ngInject */
function RoomListOpener($mdBottomSheet) {
  var directive = {
    restrict: 'A',
    link: link
  };
  return directive;

  /** @ngInject */
  function RoomListController(RoomService,$rootScope,$state) {
    var roomListCtrl = this;
   
  }

  function link(scope, element, attrs) {

   }
}

})();

A link függvénybe tegyünk egy click eseménykezelőt:


element.on( "click", function($event) {
	 
});

Az eseménykezelőben pedig használjuk az mdBottomSheet komponensét az angular materialnak:


$mdBottomSheet.show({
        templateUrl: 'app/room/room.list.tmpl.html',
        controller: RoomListController,
        controllerAs: 'roomListCtrl',
        bindToController: true,
        targetEvent: $event
      });

Itt használjuk a már definiált RoomListControllert, de nem csinál még semmit sem. Meg szeretnénk jeleníteni az összes szobát, kérjük hát le a RoomService-től:


roomListCtrl.rooms = RoomService.getRooms();

Már csak a room.list.tmpl.html hiányzik, hozzuk létre a következő tartalommal:


<md-bottom-sheet class="md-list md-has-header">
<md-subheader>Available Rooms <small>Click the room you want to open</small></md-subheader>
<md-chips ng-model="roomListCtrl.rooms" readonly="true">
  <md-chip-template>
    <strong></strong>
  </md-chip-template>
</md-chips>
</md-bottom-sheet>

Szoba megnyitása

step_room_view

Ebben a szakaszban a cél, hogy egy tabos elrendezést kialakítsunk a megnyitott szobáknak. Először is a room.service.js fájlt egészítsük ki egy szobát lekérő metódussal:


this.getRoom = getRoom;

function getRoom(roomId) {
var room = Room.get({id: roomId});
return room;
}

Létre kell hoznunk egy olyan service-t ami nyílvántartja a már megnyitott szobákat: opened.room.factory.js


(function () {
'use strict';

angular
  .module('angularChat')
  .factory('openedRoomsFactory', openedRoomsFactory);

/** @ngInject */
function openedRoomsFactory(RoomService,$localStorage,$rootScope) {
  var self = this;

  var roomFactoryObj = {};

  return roomFactoryObj;
}

})();

Egészítsük ki a következő függvényekkel: * Aktuálisan megnyitott szoba index:


  roomFactoryObj.setSelectedIndex = function(newIndex){
    $localStorage.index  = newIndex;
  };

  roomFactoryObj.getSelectedIndex = function(){
    return $localStorage.index;
  };
  • Szoba lekérése index alapján:

    
    roomFactoryObj.getRoomByIndex = function(index){
      var room = _.findWhere($localStorage.rooms, { 'index': index });
      return room;
    };
    

  • Összes megnyitott szoba lekérdezése

    
    roomFactoryObj.getRooms = function(){
    return $localStorage.rooms;
    };
    

  • Szoba hozzáadása a már megnyitott szobákhoz:

    REXML could not parse this XML/HTML: 
    <pre><code>
    roomFactoryObj.addRoom = function(room){
      if(typeof $localStorage.rooms == "undefined") {
        $localStorage.rooms = [];
      }

    room.index = $localStorage.rooms.length; $localStorage.index = room.index; $localStorage.rooms.push(room); $rootScope.$emit(“room.added”); };

REXML could not parse this XML/HTML: 
</code>
  • Szoba eltávolítása a már megnyitott szobák közül:

    
    roomFactoryObj.removeRoom = function(index){
      $localStorage.rooms.splice(index, 1);
    };
    

  • Van-e megnyitott szoba?

    REXML could not parse this XML/HTML: 
    <pre><code>
    roomFactoryObj.hasRoom = function(){
      return $localStorage.rooms && $localStorage.rooms.length>0;
    };
    </code></pre>

  • Meg van-e nyitva egy szoba?

    
    roomFactoryObj.containsRoom = function(room){
      var result = _.findWhere($localStorage.rooms, { '_id': room._id });
      return typeof result != "undefined";
    };
    

  • A factory létrejöttekor szinkronizáljunk a korábban megnyitott szobákat a local storage-ból:

    REXML could not parse this XML/HTML: 
    <pre><code>
    function syncRoomsFromLocalStorage(){
      _.forEach($localStorage.rooms,function(storedRoom){
        $localStorage.rooms[storedRoom._id] = RoomService.getRoom(storedRoom._id)
      });
    }

    syncRoomsFromLocalStorage();

REXML could not parse this XML/HTML: 
</code>

A szoba listán ha rákattintunk egy elemre akkor szeretnénk megnyitni egy szobát. Ezzel egy új navigációhoz jutunk, így az index.route.js-ben:


.state('rooms.room', {
url: '/:id',
templateUrl: 'app/room/room.item.html',
controller: 'RoomItemController',
controllerAs: 'roomItemCtrl',
data: {authenticated: true}
})

hozzuk létre az itt említett RoomItemControllert: room.item.controller.js fájl létrehozása a room mappában a következő tartalommal:


(function () {
'use strict';

angular
  .module('angularChat')
  .controller("RoomItemController", RoomItemController);

/** @ngInject */
function RoomItemController($scope, $timeout, $mdBottomSheet, toastr, RoomService, $log, $rootScope, $state, openedRoomsFactory, apiUrl, socketFactory, AccountService, allUsersFactory) {
  var roomItemCtrl = this;
  var roomId = $state.params.id;

  roomItemCtrl.users = [];
  roomItemCtrl.allusers = allUsersFactory.users;

  RoomService.getRoom(roomId).$promise.then(function (room) {
    _.forEach(room.users, function (userId) {
      if (!isUserAlreadyAdded(userId)) {
        roomItemCtrl.users.push(allUsersFactory.users[userId]);
      }
    });

    roomItemCtrl.room = room;
  });

  function isUserAlreadyAdded(userId){
    var userfound = _.findWhere(roomItemCtrl.users, {'_id': userId});
    return typeof userfound != "undefined";
  }
}
})();

Ne feledkezzünk meg a kedvenc socket.io-s eseményekre feliratkozni a kontrollerben! :)


socketFactory.emit("subscribe", {room: roomId, user: AccountService.getLoggedInUser()});

  socketFactory.on("user.joined",function (user) {
      if (!isUserAlreadyAdded(user._id)) {
        roomItemCtrl.users.push(user);
      }
  });

  socketFactory.on("user.left",function (user) {
      _.remove(roomItemCtrl.users, {
        _id: user._id
      });
  });

  $scope.$on("$destroy", function () {
    socketFactory.emit("unsubscribe", {room: roomId, user: AccountService.getLoggedInUser()});
  });

Készítsük el a szoba megjelenítésének alapjait. room.item.html létrehozása:


<div layout="row" layout-wrap>
<md-content layout="column" flex="80" flex-sm="100">
  <md-content style="height: 60vh;">
  
</md-content>
  <md-content layout-padding>

  </md-content>
</md-content>
<md-content class="side-nav room-users" hide-sm layout="column" flex="18">
  <md-list layout-fill>
    <md-subheader class="md-accent">Available users</md-subheader>
    <md-list-item class="md-2-line contact-item selected" ng-repeat="(index, contact) in roomItemCtrl.users">
      <img ng-src='' class="md-avatar" alt=""/>
      <div class="md-list-item-text compact">
        <h3></h3>
        <p>@</p>
      </div>
      <md-divider></md-divider>
    </md-list-item>
  </md-list>
</md-content>
</div>

room.list.directive.js fájlban vegyük fel az openedRoomsFactory függőséget és definiáljuk a szoba megnyitásáárét felelős függvényt:


  roomListCtrl.openRoom = function(index,room){
if(!openedRoomsFactory.containsRoom(room)){
  openedRoomsFactory.addRoom(room);
  $state.go("rooms.room",{id: room._id},{reload: false});
}else{
  $rootScope.toast("You've already opened this room!");
}
};

room.list.tmpl.html fájlban eseménykezelő hozzáadása:


ng-click="roomListCtrl.openRoom($index,$chip)"

A tabos megjelenítéshez hozzunk létre egy RoomsController-t!

room.tabs.controller.js:


(function () {
'use strict';

angular
  .module('angularChat')
  .controller('RoomsController', RoomsController);

/** @ngInject */
function RoomsController($scope, $timeout, $mdBottomSheet, toastr, RoomService, $log,$rootScope,$state,openedRoomsFactory) {
  var roomsCtrl = this;
 
   }
})();

Olvassuk be ha már volt elmentett szoba:


syncFromOpenedRoomsFactory();

function syncFromOpenedRoomsFactory(){
    roomsCtrl.selectedIndex = openedRoomsFactory.getSelectedIndex();
    roomsCtrl.rooms = openedRoomsFactory.getRooms();
  }

Ha új szobát nyitunk meg, szinkronizáljuk a mezőket:


$rootScope.$on("room.added",syncFromOpenedRoomsFactory);

Tab bezárásakor szoba eltávolítása:


roomsCtrl.removeRoom = function (index) {
    openedRoomsFactory.removeRoom(index);
  };

Tabokat ha kattintgatjuk, akkor frissítsük az indexet az openedRoomsFactory-ban is! A routingot is állítsuk be ennek megfelelően!


$scope.$watch("roomsCtrl.selectedIndex",function(newIndex){
    openedRoomsFactory.setSelectedIndex(newIndex);
    if(openedRoomsFactory.hasRoom()){
      $state.go("rooms.room",{id: openedRoomsFactory.getRoomByIndex(newIndex)._id});
    }
  });

Hozzuk létre a tabos elrendezés nézetét: room.tabs.tmpl.html a következő tartalommal:


<md-content flex ng-if="roomsCtrl.rooms.length > 0">
<md-subheader>Opened Rooms right now</md-subheader>
<md-tabs md-dynamic-height md-selected="roomsCtrl.selectedIndex" md-border-bottom md-autoselect>
  <md-tab ng-repeat="room in roomsCtrl.rooms">
    <md-tab-label> <a ng-click="roomsCtrl.removeRoom($index,room)">
      <md-icon md-svg-icon="navigation:close"></md-icon>
    </a>
    </md-tab-label>
    <div ui-view flex></div>
</md-tabs>
</md-content>

Ne feledkezzünk meg kilépéskor leiratkoztatni a szobáról a usert. Ehhez egészítsük ki az account.service.js logout függvényét és vegyük fel az openedRoomsFactory és a socketFactory függőségeket is:


var openedRooms = openedRoomsFactory.getRooms();
//logout from rooms
_.forEach(openedRooms, function (room) {
socketFactory.emit("unsubscribe", {room: room._id, user: getLoggedInUser()});
});

töröljük a megnyitott szobákat is:


delete $localStorage.rooms;
delete $localStorage.index;

A routingot is egészítsük ki az index.routes.js fájlban. Itt már létezik egy “rooms” nevű állapot, ezt egészítsük ki, hogy a következőképpen nézzen ki:


.state('rooms', {
  url: '/rooms',
  templateUrl: 'app/room/room.tabs.tmpl.html',
  controller: 'RoomsController',
  controllerAs: 'roomsCtrl',
  data: {authenticated: true}
});

A create.room.fab.directive.js-hez adjuk hozzá az openedRoomsFactory függőséget valamint atdjuk meg azt is, hogy ha új szobát hozunk létre, az egyből bekerüljön a megnyitott szobák közé és meg is nyíljon:


openedRoomsFactory.addRoom(result);
$state.go("rooms.room",{id: result._id});

Üzenetek kezelése

step_messages

Az üzenetek aspektus számára hozzuk létre a message nevű mappát. Üzenet modell: message.model.js fájl létrehozása message mappában


(function () {
'use strict';

angular.module('angularChat')
.factory('Message', Message);

/** @ngInject */
function Message(apiUrl,$resource) {
  return $resource(apiUrl + '/rooms/:id/messages/:messageId', 
{ id: '@_id' });
}

})();

Hozzunk létre egy service-t, amely az üzenetek kezeléséért lesz felelős! message.service.js message mappában


(function() {
'use strict';

angular
  .module('angularChat')
  .service('MessageService',MessageService);

/** @ngInject */
function MessageService($resource,apiUrl,Message,Room,AccountService) { 
}
})();

A Service-ben két függvényt kell megírnunk.

  • A szoba üzeneteit lekérő függvény

    
    function getRoomMessages(roomId){
    return Message.query({id: roomId});
    }
    

  • Új üzenet létrehozása a szobában

    
    function createRoomMessage(roomId, message){
    var newMessage = new Message();
    newMessage.text = message;
    newMessage.user = AccountService.getLoggedInUser();
    newMessage.$save({id: roomId});
    }
    

A két függvény publikussá tételéért vegyük fel a Service elején a következő utasításokat:


this.getRoomMessages = getRoomMessages;
this.createRoomMessage = createRoomMessage;

Hívjuk meg ezt a két függvényt az alkalmazás megfelelő pontjain!

room.item.controller.js fájlban:

A service használatához adjuk hozzá a függőségekhez! Vegyük fel a két változót:


roomItemCtrl.newMessage = "";
roomItemCtrl.messages = MessageService.getRoomMessages(roomId);

Új üzenet létrehozása:


roomItemCtrl.createMessage = function () {
MessageService.createRoomMessage(roomId, roomItemCtrl.newMessage);
roomItemCtrl.newMessage = "";
};

Sockethez tartozó rész:


socketFactory.on("new message",function (message) {
roomItemCtrl.messages.push(message);
});

Nyissuk meg a room/room.item.html template-et! Készítsünk egy angular material-os listát, melyben a roomItemCtrl.messages változót jelenítjük meg:


<md-list scroll="roomItemCtrl.messages">
<md-subheader class="md-info">Messages in room </md-subheader>
<message ng-repeat="message in roomItemCtrl.messages" message="message"
         author="roomItemCtrl.allusers[message.authorId]"
         ng-class="{ 'repeated-author' : $index>0 && message.authorId == roomItemCtrl.messages[$index-1].authorId}"></message>
</md-list>

Megjegyzés: • az ng-class rész az üzenetek megjelenítésének testreszabására van.

Új üzenet létrehozására:


<md-input-container class="md-accent">
<label>New Message</label>
<input ng-enter="roomItemCtrl.createMessage()" ng-model="roomItemCtrl.newMessage" md-maxlength="350"/>
</md-input-container>

Az előzőekben használtuk a message direktívát, de az sehol sem létezik, ezt sajnos nekünk kell megírnunk :) A message mappában hozzuk létre a message.directive.js fájlt:


(function() {
'use strict';

angular
  .module('angularChat')
  .directive('message', message);

/** @ngInject */
function message() {
  var directive = {
    restrict: 'E',
    templateUrl: 'app/message/message.item.tmpl.html',
    controller: MessageController,
    controllerAs: 'messageCtrl',
    bindToController: true,
    scope:{
      message: "=",
      author: "="
    }
  };

  return directive;

  /** @ngInject */
  function MessageController($mdDialog){
    var messageCtrl = this;
  }
}

})();

Túl sok mindent nem csinál, megjeleníti a megadott template-et, ami még nem létezik. Hozzuk létre a hiányzó message.item.tmpl.html fájlt a következő tartalommal:


<md-list-item class="contact-item md-2-line selected">
<img ng-src="" class="md-avatar message-avatar"/>
<div class="md-list-item-text compact">
  <p style="text-align: right;"><small><strong></strong>, </small></p>
  <div class="message-content"></div>
</div>
</md-list-item>

Az md-list direktíva elemeit akarjuk megadni, így az md-list-item kötelező.

Openlayers battles - Is this polygon convex or not?

Once upon a time there was a request from the customer to be able to draw polygons on a map. Later there were more and more feature requests to transfom this polygon editor to a swiss army knife. One of them was to prevent users to draw concave polygons on the map.

This would be pretty easy - I thought - if it were only a wonderful Openlayers function e.g polygon.isConvex(). But it wasn’t, or at least I could find it in version 2.13.1. I was searching the internet, trying to find the simplest solution, because I was sure there is a more elegant and shorter solution than hacking with angles.
Finally StackOverflow came to my help (as always :). In this thread I found the most suitable algorithm for my need.

A polygon is a set of points in a list where the consecutive points form the boundary. It is much easier to figure out whether a polygon is convex or not (and you don't have to calculate any angles, either):
For each consecutive pair of edges of the polygon (each triplet of points), compute the z-component of the cross product of the vectors defined by the edges pointing towards the points in increasing order. Take the cross product of these vectors:
 given p[k], p[k+1], p[k+2] each with coordinates x, y:
 dx1 = x[k+1]-x[k]
 dy1 = y[k+1]-y[k]
 dx2 = x[k+2]-x[k+1]
 dy2 = y[k+2]-y[k+1]
 zcrossproduct = dx1*dy2 - dy1*dx2
The polygon is convex if the z-components of the cross products are either all positive or all negative. Otherwise the polygon is nonconvex.
If there are N points, make sure you calculate N cross products, e.g. be sure to use the triplets (p[N-2],p[N-1],p[0]) and (p[N-1],p[0],p[1]).

You can checkout my Javascript implementation of this algorithm here.

Openlayers concave shape example

Batman vs Superman - AngularJS 2 example with Firebase

Lately I had the opportunity to perform on Google IO 2015 Extended here in Hungary. My topic was AngularJS 2.0, so I made a simple application to present its basic features.

You can checkout the demo application here.

Code of the application is available here. If you have any comments or improvement ideas, feel free to share with me!