Тестовое задание. Проверка вхождения точки в произвольный полигон

88d3825bef9747128221b28df68358e2.png

Вводная


Сразу оговорюсь кому может быть интересна данная публикация. Это начинающие Django + JQuery программисты, интересующиеся векторной графикой в браузере с использованием canvas. Или просто люди, получившие подобное задание.
Итак, находясь в постоянном сканировании рынка труда своего региона, наткнулся на весьма интересную вакансию web-разработчика в достаточно известной местной компании. В описании вакансии было сказано, что нужен python+django разработчик. После отправки резюме получил тестовое задание которое гласило:

Необходимо создать веб-приложение на Django, которое определяет факт вхождения точки в произвольный (не выпуклый) полигон. Клиентская часть должна отобразить в браузере (на канвасе или svg, или еще на чем-нибудь, в общем не принципиально) произвольный полигон, позволить пользователю указать точку на экране, отправить запрос на сервер, получить и отобразить ответ. Серверная часть, соответственно, должна принять запрос, определить, находится точка внутри контура, или нет, и вернуть ответ клиенту. Серверная часть на Python, клиентская — HTML + JavaScript либо CoffeeScript.


Потратив пару часов на выполнение задания и публикацию результата на тестовом сервере я уперся в полный игнор со стороны потенциального работодателя. Я не в обиде, бывает всякое, тем более задание было интересное и его выполнение принесло немало удовольствия. Чтобы добро не пропадало — публикую его здесь.

Поехали


Первым делом подготовим площадку, я использовал virtualenv:

scherbin@scherbin-pc ~$ cd WebDev/venvs
scherbin@scherbin-pc ~/WebDev/venvs/ $ virtualenv test
scherbin@scherbin-pc ~/WebDev/venvs/ $ cd test
scherbin@scherbin-pc ~/WebDev/venvs/test/ $ source bin/activate


Устанавливаем необходимый софт и прямо здесь создаем проект:

(test)scherbin@scherbin-pc ~/WebDev/venvs/test/ $ pip install django
(test)scherbin@scherbin-pc ~/WebDev/venvs/test/ $ django-admin startproject mytest
(test)scherbin@scherbin-pc ~/WebDev/venvs/test/ $ cd mytest


Наш проект будет состоять из двух приложений start и api. Первое будет отдавать браузеру пользователя HTML и JavaScript то есть наш frontend, второе будет обрабатывать AJAX запросы frontend-а, то есть будет нашим backend-ом. Создаем их:

(test)scherbin@scherbin-pc ~/WebDev/venvs/test/mytest/ $ django-admin startapp start
(test)scherbin@scherbin-pc ~/WebDev/venvs/test/mytest/ $ django-admin startapp api


Структура


Теперь, когда костяк проекта у нас есть, можно переходить непосредственно к творчеству. Для начала распишем структуру проекта. Как было указано выше, он будет состоять из двух частей, frontend и backend.Frontend
Будет выводить в браузере, с использованием canvas, произвольный полигон и обрабатывать клики пользователя по нему. При клике backend-у посредством AJAX запроса будут передаваться две вещи в формате JSON:

  1. Массив координат полигона.
  2. Координаты точки куда кликнул пользователь.


После получения ответа на его основе в точке клика будет рисоваться круг диаметром 5 пикс. зеленого (входит в полигон) или красного (не входит) цвета.Backend
Принимает запрос от frontend-а, вычисляет принадлежность точки полигону и возвращает результат в виде координат точки и boolean признака вхождения. Как видим многоугольник не хранится на сервере и есть потенциальная возможность использования и backend-а в случаях когда многоугольник меняется.

Реализация


Первым делом нам надо вывести наш полигон в браузере пользователя. Создаем главную (и единственную в нашем мини проекте) HTML страницу.

mytest/start/templates/start/index.html:




    
    Тестовое задание
    {% load staticfiles %}
    
    


    
    















Как видно из кода на странице используется библиотека JQuery версии 1.12.0 и наш main.js файл где содержится JavaScript код, реализующий всю рутину. А именно рисование полигона, обработку кликов и связь с backend-ом. По сути это главный файл нашего мини проекта.

mytest/start/static/js/main.js:

$(function() {
    /**
     * Координаты нашего полигона
     */
    var polygon = [
        [200, 50],
        [415, 80],
        [550, 200],
        [700, 200],
        [300, 400],
        [750, 580],
        [150, 530],
        [100, 150],
        [300, 250],
        [200, 50]
    ];

    /**
     * Размеры холста
     */
    var canvasWidth = 800;
    var canvasHeight = 600;

    /**
     * Функция вывода нашего полигона на холсте с использованием массива координат
     */
    var drawPolygon = function(id, coords) {
        var canvas = $("#"+id);

        if (canvas.length) {
            var context = canvas[0].getContext("2d");

            context.canvas.width = canvasWidth;
            context.canvas.height = canvasHeight;

            context.beginPath();

            for (var i = 0; i < coords.length; i++) {
                if (i == 0) context.moveTo(coords[i][0], coords[i][1]);
                else context.lineTo(coords[i][0], coords[i][1]);
            }

            context.strokeStyle = '#0000';
            context.stroke();
        }
    };

    /**
     * Обрабатываем нажатия мышкой на холст
     */
    $(document).on("click", "#canvas-main", function(event){
        // ФИксируем  координаты клика
        var x = event.pageX-$(this).offset().left;
        var y = event.pageY-$(this).offset().top;
        // Готовим запрос к серверу. Запрос содержит координаты полигона и точки куда был произведен клик
        var query = {
            "polygon": polygon,
            "point": [x, y]
        };
        // Получаем доступ к холсту
        var context = $(this)[0].getContext("2d");

        // Выполняем POST запрос к серверу
        $.ajax({
            type: "POST",
            url: '/api/in_polygon',
            data: JSON.stringify(query),
            success: function(data) {
                // ОБрабатываем полученный ответ
                p = data['point'];

                // Рисуем круг в точке клика
                context.beginPath();
                context.arc(p[0], p[1], 5, 0, 2 * Math.PI, false);

                // По результату запроса заливаем круг зеленым или красным цветом
                if (data['in_polygon'])
                    context.fillStyle = "#73AD21";
                else
                    context.fillStyle = "#FF0000";

                context.fill();
            }
        });
    });

    /**
     * Рисуем полигон сразу после загрузки страницы
     */
    drawPolygon("canvas-main", polygon);
});


Теперь необходимо реализовать саму проверку вхождения точки в полигон. Алгоритм использован самый простейший — трассировка луча. Последовательно проверяются все грани полигона на пересечение с лучом, идущим из точки, куда кликнул пользователь. Четное количество пересечений или нет пересечений вовсе — точка за пределами полигона. Количество пересечений нечетное — точки внутри. Далее python реализация в backend приложении api.

mytest/api/views.py:

# -*- coding: utf-8 -*-
import json
from django.http import HttpResponse, JsonResponse

# Create your views here.


def in_polygon(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        in_polygon = False

        # Main algorithms
        x = data['point'][0]
        y = data['point'][1]
        for i in range(len(data['polygon'])):
            xp = data['polygon'][i][0]
            yp = data['polygon'][i][1]
            xp_prev = data['polygon'][i-1][0]
            yp_prev = data['polygon'][i-1][1]
            if (((yp <= y and y < yp_prev) or (yp_prev <= y and y < yp)) and (x > (xp_prev - xp) * (y - yp) / (yp_prev - yp) + xp)):
                in_polygon = not in_polygon

        response = {u'in_polygon': in_polygon, u'point': [x, y]}
        return JsonResponse(response)
    else:
        return HttpResponse(u'Запрос должен использовать метод POST');


Теперь основной функционал готов. Осталось заставить все это дело работать. Для этого правим три файла.

mytest/mytest/settings.py

# Application definition

INSTALLED_APPS = [
    ...
    'start',
    'api',
]

mytest/mytest/urls.py

from django.conf.urls import *
from django.contrib import admin
from start.views import *

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/', include('api.urls')),
    url(r'^$', start_index),
]

mytest/api/urls.py

from django.conf.urls import *
from api.views import *

urlpatterns = [
    url(r'^in_polygon$', in_polygon, name='in_polygon')
]


С этого момента можно запускать встроенный в Django тестовый web сервер:

(test)scherbin@scherbin-pc ~/WebDev/venvs/test/mytest/ $ ./manage.py runserver


И играть в зеленые и красные точки перейдя в браузере по адресу localhost:8000/. Должна получиться картинка, аналогичная той, что в начале поста.

© Habrahabr.ru