SQL Server и MapPoint

Совместная работа MapPoint 2010 и SQL Server Spatial

Эрик Фрост (Eric Frost) и Ричард Марсден (Richard Marsden)

Загрузить образец кода

Если не считать сервис Bing Maps, две наиболее заметные геопространственные технологии от Microsoft — это Microsoft MapPoint 2010 и соответствующая функциональность в SQL Server 2008 R2. Однако, хотя SQL Server является идеальным хранилищем геопространственных данных и MapPoint — хорошее средство визуализации таких данных, взаимодействие между этими продуктами осуществляется не столь просто, как могло бы быть.

В этой статье будет продемонстрировано, как считывать объекты точек и полигонов из SQL Server и визуализировать их в MapPoint. Мы также покажем, как записывать точки и полигоны обратно в SQL Server, используя Entity Framework 4.0, которая включена в Visual Studio 2010.

Для иллюстративных целей мы будет использовать адреса ресторанов чикагской компании «Al’s Beef» и гипотетические торговые области. В анализе и моделировании розницы торговые области можно определять на основе различных параметров и применять в разных целях. Как правило, это наименьший участок вокруг магазина, в который входят области, отвечающие конкретному пороговому значению, например области, где живут или работают 50 или 75% клиентов. Все торговые области, используемые в этой статье, были сгенерированы с помощью инструмента Create Drivetime Zone в MapPoint, поэтому они представляют гипотетические торговые области, определенные на основе времени подъезда к ним.

Как сеть менее чем с несколькими десятками точек Al’s Beef — сравнительно небольшое предприятие, но те же концепции и методики можно применять к крупным розничным компаниям с тысячами точек, а также к предприятиям из других отраслей.

Оба примера кода и набор данных «Al’s Beef» (как SQL-сценарий) можно скачать по ссылке code.msdn.microsoft.com/mag201009Spatial.

Наша статья не является излишне технической, в которой описываются заумные аспекты новейшего языка или технологии, — это скорее практическое руководство по тому, как подружить две распространенные технологии Microsoft. Парочка барьеров включает неспособность Entity Framework напрямую распознавать географические объекты и требование SQL Server Spatial к размещению полигонов против часовой стрелки, чего как раз не требуется MapPoint. Надеемся, эта статья поможет даже искушенным разработчикам (которые, возможно, не хотят сейчас браться за работу в этой области из-за недостатка соответствующего опыта) и в то же время покажет разработчикам, использующим MapPoint, как успешно взаимодействовать с SQL Server 2008 R2.

Подготовка базы данных

Чтобы вместе с нами следовать примерам кода в статье, скачайте SQL-сценарий и выполните его в SQL Server для подготовки базы данных и объектов. Данные хранятся в базе данных SQL Server под названием «Corporate», которая включает одну таблицу, одно представление и одну хранимую процедуру. Адреса точек компании «Al’s Beef» хранятся в таблице Locations (рис. 1).

image: The Table and View Included in the Sample Database

Рисунок 1 Таблица и представление, включенные в образец базы данных

Она включает адреса магазинов, атрибуты (например, можно ли въехать в магазин на машине?) и географический тип данных координат местонахождения. Полигоны гипотетических торговых областей также хранятся в этой таблице в поле TradeArea с использованием географического типа данных.

Представление vLocations помещает географические поля с точками и полигонами в типы данных, которые понятны Entity Framework и могут быть считаны этой инфраструктурой.

Поле географической точки разбивается на поля широты и долготы и возвращается клиенту как поле varbinary. Это вызвано тем, что Entity Framework не может напрямую манипулировать географическими типами данных, но способна обрабатывать поля varbinary. Впоследствии приложение может преобразовать их обратно в географические объекты.

Вот хранимая процедура uspAddLocation, которая, как и предполагает ее название, используется для вставки новых адресов из MapPoint обратно в SQL Server:

CREATE VIEW [dbo].[vLocations]
AS
SELECT LocID,Location.Long As Longitude,
       Location.Lat As Latitude,
       CAST(Location AS VARBINARY(MAX)) AS Location,
       Locations.TradeArea.STAsText() As TradeAreaWKT,
       CAST(TradeArea AS VARBINARY(MAX)) AS TradeArea
FROM Locations

Мы еще вернемся к этой хранимой процедуре.

Подготовка приложения

Наш проект является приложением Windows Forms на C#, которое включает элемент управления MapPoint. Этот элемент поставляется с MapPoint 2010, поэтому на вашем компьютере должна быть установлена полная версия MapPoint 2010. По записям можно перемещаться с помощью кнопок для отображения магазина и его торговой области. Магазины также можно выбирать щелчком значка метки магазина. Кроме того, на форме присутствует флажок для отображения торговой области как выпуклой оболочки множества точек (convex hull) и кнопка для добавления новых адресов. По умолчанию приложение показывает полигон в том виде, в каком он хранится в базе данных (рис. 2).

image: The Al’s Beef App, Showing the Chicago Heights Store and Territory as Defined in the Database

Рисунок 2 Приложение Al’s Beef, показывающее магазин Chicago Heights и территорию, как определено в базе данных

Если флажок View Trade Area As Convex Hull установлен, торговая область обводится линией (выпуклой оболочкой множества точек) по аналогии с тем, как полигон обводится «резиновой» полоской (rubber band) (рис. 3).

image: The Chicago Heights Store, with a Convex Hull Wrapped Around the Territory Shown in Figure 2

Рисунок 3 Магазин Chicago Heights, территория которого (рис. 2) обведена выпуклой оболочкой множества точек

Прежде чем реализовать экран с картой, нужно добавить Entity Data Objects, которые указывают на таблицу и представление базы данных. Чтобы установить Entity Data Objects, щелкните правой кнопкой мыши приложение в Visual Studio Solution Explorer и выберите Add | New Item | Visual C# Items | ADO.NET Entity Data Model. Щелкните Add и укажите Generate from Database. В диалоге Choose Your Database Objects выберите таблицу Locations и представление vLocations. После того как вы щелкнете Finish, мастер создаст объекты и сгенерирует код, необходимый для соединения с базой данных.

Чтобы добавить элемент управления MapPoint 2010 в Windows Forms, сначала нужно добавить COM-компонент элемента управления MapPoint в окно инструментария Visual Studio. COM — не самая модная технология, но она по-прежнему является важной частью экосистемы Windows. Многие приложения, в том числе MapPoint, реализуют API только через COM-интерфейс, и поддержка COM в Visual Studio исчезнет еще не скоро.

Откройте окно инструментария Visual Studio и в разделе General щелкните правой кнопкой мыши и выберите Choose Items. Перейдите на вкладку COM Components и укажите Microsoft MapPoint Control 17.0. Версия 17.0 как раз и относится к MapPoint 2010 (Северная Америка или Европа). Более старые версии MapPoint (от 2002 и выше) тоже можно использовать, но придется вносить небольшие изменения в имена (например, в имя панели инструментов и идентификатор символа).

В сборках AxInterop.MapPoint и Interop.MapPoint задайте свойство Embed Interop Type как False, а свойство Copy Local — как True.Теперь элемент управления MapPoint можно перетаскивать на форму и использовать в приложении.

Инициализации формы Map: загрузка MapPoint

В форме Map объявляется несколько переменных-членов, в том числе соединение базы данных с Entity Framework, список сведений о магазинах и параллельный список географических данных этих магазинов, которые будут считываться из представления. Переменная curLoc отслеживает идентификатор текущего магазина в приложении, а objMap ссылается на объект карты в элементе управления MapPoint:

namespace AlsBeef
{
 public partial class Map : Form
 {
  CorporateEntities db;
  List<Location> locationList;
  List<vLocation> vlocationList;
  int curLoc = -1;    // <0 indicates 'not set'
  MapPoint.Map objMap;
  MapPoint.Symbol objSymb;
  ...

При создании формы вызывается метод CreateNewMapObject для инициализации картографического элемента управления и открывается новая карта с использованием по умолчанию шаблона для Северной Америки. Настраиваются панели инструментов, определяется objMap и, чтобы не загромождать карту, отключается функция Point of Interest (рис. 4). Points of Interest — это заранее определенные места MapPoint, например рестораны и театры.

Рисунок 4 Создание формы

public Map()
{
 InitializeComponent();
 CreateNewMapObject();
}


private void CreateNewMapObject()
{
  MPctrl.NewMap(GeoMapRegion.geoMapNorthAmerica);
  object barObj = "advanced";
  MPctrl.Toolbars.get_Item(refbarObj).Visible = true;
  MPctrl.Toolbars.LargeToolbarButtons = false;
  objMap = MPctrl.ActiveMap;
  // Make sure all points of interest are turned off
  objMap.PlaceCategories.Visible = MapPoint.GeoTriState.geoFalse;
}

Метод Load формы заполняет оба списка сведений о магазинах. locationList содержит всю информацию, отличную от географической, а vloc­ationList считывает поля географических данных, которые преобразуются представлением базы данных:

private void Map_Load(object sender, EventArgs e)
{
 db = new CorporateEntities();
 locationList = new List<Location>();
 vlocationList = new List<vLocation>();

 ObjectQuery<Location> locationQuery =
  db.Locations;
 ObjectQuery<vLocation> vlocationQuery =
  db.vLocations;

 locationList = locationQuery.ToList();
 vlocationList = vlocationQuery.ToList();

 InitMapSymb();
 InitMapPins();
 SetLocation(0);
}

Последние две строки фактически запускают приложение, инициализируя карту. Они добавляют метку (pushpin) для каждого адреса магазина (InitMapPins) и настраивают элементы управления карты и формы так, чтобы они указывали на данные для первого магазина (SetLocation).

Добавление меток на карту

Код становится интереснее в методе InitMapPins:

private void InitMapPins()
  {
    MapPoint.Pushpin objPin = null;
    for (int i = 0; i < locationList.Count;
     i++)
    {
     MapPoint.Location objLoc =
      objMap.GetLocation(vlocationList[i].    
      Latitude.Value,
      vlocationList[i].Longitude.Value);
     objPin = objMap.AddPushpin(objLoc,   
      locationList[i].Name);
     objPin.Symbol = 145; // Red fork and knife
                         // (food, restaurant)
   }
  }

Перебирая locationList в цикле, мы извлекаем значения широты и долготы, вычисленные представлением. Они используются для создания объектов MapPoint Location, и с их помощью потом создаются метки на карте (MapPoint-объекты Pushpin). Название магазина помещается в свойство PushpinName, которое впоследствии будет использоваться для поиска и позиционирования на карте. В данном случае метки показываются встроенным в MapPoint красным символом ресторана (символ #145). Полный список встроенных в MapPoint 2010 символов см. по ссылке mapping-tools.com/info/pushpins/pushpins_2010.shtml. На этой странице также есть ссылки на списки меток для более ранних версий MapPoint.

Позиционирование в текущую запись и добавление полигонов на карту

Новые записи выбираются и отображаются с помощью методов IncDecLocation и SetLocation. Первый просто увеличивает значение текущей позиции (curLoc) на значение cnt и передает новую позицию в SetLocation:

private void IncDecLocation(int cnt = 0)
{
 // Apply the increment/decrement, wrapping around if necessary
 int newLoc = (curLoc + cnt + locationList.Count) % locationList.Count;

 SetLocation(newLoc);
}

Метод SetLocation — рабочая лошадка всего приложения. SetLocation выбирает новую позицию и отображает ее на карте. SetLocation также снимает выделение с предыдущей метки (если таковая была) и стирает с карты все предыдущие полигоны торговых областей (рис. 5).

Рисунок 5 Метод SetLocation — рабочая лошадка приложения

private void SetLocation(int newLoc)
{
  MapPoint.Pushpin objPin = null;

  // Unhighlight previous pushpin
  If (curLoc>= 0)
  {
    objPin = (MapPoint.Pushpin)
     objMap.FindPushpin(locationList[curLoc].Name);
    objPin.Highlight = false;
  }

  // Clear all previous shapes
  while(objMap.Shapes.Count> 0)
  {
    objMap.Shapes[1].Delete();
  }
 

  // Set the new location
  curLoc = Math.Min( Math.Max(newLoc,0), locationList.Count-1);

  objPin = (MapPoint.Pushpin)
   objMap.FindPushpin(locationList[curLoc].Name);
  objMap.Location = objPin.Location;

...

Следующий раздел требует некоторого трюкачества. Во-первых, приложение проверяет состояние флажка View Trade Area As Convex Hull. Если он не установлен, оно берет WKT-строку (Well-Known Text), которая определяет полигон и передает ее в собственный метод RenderPolygon для разбора и визуализации полигона на карте.

Если флажок помечен, оно извлекает объект varbinary полигона территории и рпеобразует его в географический объект, используя класс System.IO.MemoryStream и метод BinaryReader. STConvexHull — один из методов, включенных в SQL Server 2008 и позволяющих модифицировать экземпляры географических или геометрических данных. STConvexHull, в частности, работает только с геометрическими типами данных. О разнице между геометрическими и географическими типами данными SQL Server много написано в других материалах, а пока считайте, что геометрические данные определяются на двухмерной эвклидовой координатной плоскости, тогда как географические данные проецируются на сфероидальную земную поверхность с использованием системы сферических координат (нулевой уровень, проекция, осевой меридиан и размерность).

Торговая область хранится в базе данных в поле с типом географических данных, а в представлении приводится к varbinary. Это нужно для считывания в географический объект, который потом может быть преобразован в геометрический объект для выполнения метода STConvexHull.

Поскольку охватываются лишь небольшие площади, вычисления, выполняемые STConvexHull над (планарным) геометрическим объектом, практически те же, что и в случае расчета выпуклой области множества точек для реального географического объекта в системе сферических координат.

В следующей части SetLocation исходная торговая область рисуется тонкой черной линией, а затем визуализируется выпуклая область множества точек как более толстая красная линия. Код показан на рис. 6.

Рисунок 6 Рисование исходной торговой области и выпуклой оболочки множества точек

...
 // Draw trade area
if (checkBox1.Checked == false)
{
 RenderPolygon(vlocationList[curLoc].TradeAreaWKT);
}
else
{
  // Need to add C:\Program Files\Microsoft SQL
  // Server\100\SDK\Assemblies\Microsoft.SqlServer.Types.dl
  // to references
  SqlGeographyTradeAreaGeog = new SqlGeography();
  using (var stream = new
   System.IO.MemoryStream(vlocationList[curLoc].TradeArea))
  {
   using (var rdr = new System.IO.BinaryReader(stream))
   {
    TradeAreaGeog.Read(rdr);
   }
  }
  SqlGeometry TAConvexHullGeom = new SqlGeometry();
  TAConvexHullGeom =  
   SqlGeometry.STPolyFromText(TradeAreaGeog.STAsText(), 4326);
  TAConvexHullGeom = TAConvexHullGeom.STConvexHull();
  RenderPolygon(TradeAreaGeom.ToString(), 3355443, 0); // Gray80
  RenderPolygon(TAConvexHullGeog.ToString());
}
...

Как же выглядит эта WKT-строка и что с ней делает RenderPolygon? Результаты вы уже видели (на рис. 2 и 3). Давайте покопаемся во внутренностях.

WKT — это стандартный формат Open Geospatial Consortium (OGC) для форматирования геопространственных векторных данных в виде текста. WKT-строка полигона выглядит примерно так (с большими сокращениями):

POLYGON ((-111.918823979795 33.6180476378649, -111.91810682416 33.6096635553986, -111.911686453968 33.6078672297299, -111.907403888181 33.599476357922, -111.907403888181 33.6060674674809, -111.903121406212 33.6060674674809))

Списку координат, заключенных в двойные круглые скобки, предшествует слово «POLYGON». Индивидуальные пары координат разделяются запятыми. Мы используем MapPoint-метод AddPolyLine для рисования полигона на карте и его добавления в MapPoint-набор Shapes. Он принимает массив MapPoint-объектов Location как параметр. Преобразование WKT-строки в массив объектов Location требует нескольких строк кода. RenderPolygon выполняет эту задачу, отбрасывая префикс «POLYGON» и круглые скобки до того, как разбивать строку на координаты, учитывая разделители (запятые). Затем индивидуальные координаты группируются в пары чисел двойной точности (широта и долгота), которые используются для создания объектов Location. Полученный массив этих объектов передается в AddPolyline для создания нового полигона.

RenderPolygon принимает дополнительные параметры — цвет и толщину линии (рис. 7).

Рисунок 7 Метод RenderPolygon

private void RenderPolygon(string polystring, 
 int forecolor = 128, int weight = 3)
{
  polystring = polystring.Replace("POLYGON ((", "");
  polystring = polystring.Replace("))", "");
  string[] stringList = polystring.Split(',');
  MapPoint.Location[] objLoc = 
   new MapPoint.Location[stringList.Count()];
  for (int i = 0; i <stringList.Count(); i++)
  {
   string[] coords = stringList[i].Trim().Split(' ');
   objLoc[i] = objMap.GetLocation(Convert.ToDouble(coords[1]),  
    Convert.ToDouble(coords[0]), 0);
  }
  MapPoint.Shape objShape;
  objShape = objMap.Shapes.AddPolyline(objLoc);
  objShape.Line.ForeColor = forecolor;
  objShape.Line.Weight = weight;
}

Более полная версия RenderPolygon могла бы принимать дополнительные параметры, указывающие, надо ли заполнять фигуру, цвет заливки, имя фигуры (внутреннюю строку, которую можно присваивать любым фигурам) и zOrder (для размещения фигуры поверх или позади дорог и других фигур).

Рисунки и аннотации на карте MapPoint может размещать как пользователь, так и программа. MapPoint поддерживает до 40 цветов для этой аннотации. Хотя интерфейс программирования поддерживает стандартный трехбайтовый RGB (16 777 216 цветов), на самом деле эти числа — просто удобный способ задания используемого цвета. 40 цветов, поддерживаемых MapPoint, можно увидеть на веб-странице mapping-tools.com/info/pushpins/colors.shtml.

Изначально это ограничение помогало эффективнее обновлять изображения, но теперь оно в основном обеспечивает четкое различие цветов на карте.

И, наконец, мы подошли к последней части SetLocation (рис. 8).

Рисунок 8 Последняя часть SetLocation

...
   // Reset zoom level
   objMap.Altitude = 30;
   objPin.Highlight = true;

   Double distance;
   distance = 
     NearestLocation(curLoc) * 0.000621371192; //convert to miles

   label1.Text = "ID: " + locationList[curLoc].LocID.ToString();
   label2.Text = locationList[curLoc].Name + " - " +  
     locationList[curLoc].Format;
   label3.Text = locationList[curLoc].Address + ", " + 
     locationList[curLoc].City + ", " + locationList[curLoc].State;
   label4.Text = "Distance to next closest store: " + 
     String.Format("{0:#,0.0}", distance) + " miles";

  }

private double NearestLocation(int curLoc)
{

 SqlGeography AllLocations = new SqlGeography();
  SqlGeography CurLocation = new SqlGeography();
  for (int i = 0; i <locationList.Count; i++)
  {
    SqlGeography TempLocation = new SqlGeography();
    using (var stream = new 
     System.IO.MemoryStream(vlocationList[i].Location))
   { 
     using (var rdr = new System.IO.BinaryReader(stream))
     {
       TempLocation.Read(rdr);
     }
   }
   if (i == curLoc)
   {
     CurLocation = TempLocation;
   }
   else
   {
     AllLocations = AllLocations.STUnion(TempLocation);
   }
  }
  return (Double)AllLocations.STDistance(CurLocation); //meters
}

Этот код выделяет новую метку, задает уровень масштабирования (используя свойство Altitude объекта Map), сообщает информацию о магазине (из массива locationList) и определяет расстояние до ближайшего магазина.

Это расстояние вычисляется методом NearestLocation. Данный метод перебирает в цикле местоположения использует метод STUnion из SQL Server Spatial для объединения географических точек Location в экземпляр географического объекта MultiPoint. Исключением является местоположение текущего магазина, которое пропускается; иначе расстояние всегда было бы равно нулю! Затем приложение вызывает метод STDistance для вычисления расстояния в метрах между текущим местоположением и экземпляром географического объекта MultiPoint. STDistance сообщает расстояние экземпляру MultiPoint как кратчайшее между любой точкой, составляющей MultiPoint.

Кнопка для добавления нового местоположения удаляет с карты любые полигоны торговых областей, а затем просто меняет форму курсора мыши на перекрестие:

private void button1_Click(object sender, EventArgs e)
{
  // Clear all previous shapes
  while(objMap.Shapes.Count > 0)
  {
    objMap.Shapes[1].Delete();
  }
  MPctrl.MousePointer = MapPoint.GeoPointer.geoPointerCrosshair;
}

Чтобы выполнять операции с событиями MapPoint, в форму нужно добавить обработчик событий, определяемый в дизайнере формы. События можно добавлять с помощью этого дизайнера или самостоятельно в файле Map.Designer.cs. Обработчики добавляются для двух событий MapPoint: SelectionChange и BeforeClick (рис. 9).

Рисунок 9 Добавление обработчиков для событий MapPoint

// 
// MPctrl
// 
this.MPctrl.Enabled = true;
this.MPctrl.Location = new System.Drawing.Point(13, 13);
this.MPctrl.Name = "MPctrl";
this.MPctrl.OcxState = 
  ((System.Windows.Forms.AxHost.State)
  (resources.GetObject("MPctrl.OcxState")));
this.MPctrl.Size = new System.Drawing.Size(674, 389);
this.MPctrl.TabIndex = 0;
this.MPctrl.SelectionChange += 
  new AxMapPoint._IMappointCtrlEvents_SelectionChangeEventHandler
 (this.MPctrl_SelectionChange);
this.MPctrl.BeforeClick += 
  new AxMapPoint._IMappointCtrlEvents_BeforeClickEventHandler
  (this.MPctrl_BeforeClick);

Событие SelectionChange позволяет обнаруживать, выбрал ли пользователь какую-то метку. Это используется для перемещения текущей записи в запись этой метки. Реализация обработчика этих событий показана на рис. 10.

Рисунок 10 Реализация обработчика событий SelectionChange

private void MPctrl_SelectionChange(object sender,  
 AxMapPoint._IMappointCtrlEvents_SelectionChangeEvent e)

{
  // Has the user just selected a pushpin?
  if (e.pNewSelection is MapPoint.Pushpin)
  {
    MapPoint.Pushpin ppin = e.pNewSelection as MapPoint.Pushpin;

    // Find the corresponding location object, and select it
    for (int iloc = 0; iloc < locationList.Count; iloc++)
    {
      if (locationList[iloc].Name == ppin.Name)
      { // Found it: select, and move to it
        SetLocation(iloc);
        break;
      } 
    } 
  } 
}

Этот код проверяет, действительно ли только что выбранный объект является меткой. Потом выполняется простой поиск подходящей записи по локальному locationList. Метки MapPoint могут иметь дублирующиеся имена, поэтому в данном коде предполагается, что у всех записей для местоположений (а значит, и у меток) уникальные имена. Если вы точно не знаете, есть ли имена-дубликаты, то обязательно сравнивайте географические координаты.

Обработчик событий BeforeClick карты используется в функции добавления нового местоположения магазина. Обработчик проверяет, имеет ли курсор мыши форму перекрестия (т. е. пытается ли пользователь вставить новое местоположение). Это позволяет MapPoint обрабатывать событие щелчка, если курсор мыши отличен от перекрестия. Если же курсор имеет форму перекрестия, программа захватывает щелчок и добавляет новую метку под курсором мыши, используя красный символ ресторана. В этот момент, чтобы упростить работу, программа — вместо того чтобы заставлять пользователя рисовать торговую область — вызывает MapPoint-метод AddDrivetimeZone для генерации гипотетической (на основе времени подъезда) торговой области вокруг нового участка.

Чтобы сохранить эту фигуру в SQL Server, программа сначала раскладывает ее на вершины, которые потом преобразуются в WKT-определение полигона (текст). После этого осуществляется запись в базу данных SQL Server.

Чтобы передать точку и полигон обратно в SQL Server и обновить поля с географическими данными, хранимые процедуры, поддерживаемые Entity Framework, использовать нельзя, так как этот тип данных не поддерживается. Однако, поскольку Entity Framework 4.0 теперь поддерживает выполнение произвольных хранимых процедур, мы можем импортировать хранимую процедуру и выполнить ее как обычную функцию.

Следующий код задает параметры и выполняет хранимую процедуру usp­AddLocation:

object[] parameters =
{   
  new SqlParameter("Latitude",objLoc.Latitude),
  new SqlParameter("Longitude",objLoc.Longitude),
  new SqlParameter("PolyWKT",PolyWKT)
};

var retValue = db.uspAddLocation(objLoc.Longitude, 
 objLoc.Latitude, PolyWKT);

Наконец, эта процедура восстанавливает начальное состояние карты (CreateNewMapObject), заново запрашивает список местоположений из базы данных (InitMapPins) и выбирает новый магазин как текущую запись (SetLocation):

// Re-query and re-initialize map
 ObjectQuery<Location> locationQuery = db.Locations;
 ObjectQuery<vLocation> vlocationQuery = db.vLocations;
 locationList = locationQuery.ToList();
 vlocationList = vlocationQuery.ToList();
 objMap.Saved = true;
 CreateNewMapObject();
 InitMapSymb();
 InitMapPins();
 SetLocation( locationList.Count – 1 );
 e.cancel = true;
}

Строка e.cancel=true запрещает MapPoint дальнейшую обработку события щелчка. Что представляет собой хранимая процедура uspAddLocation, показано на рис. 11.

Рисунок 11 Хранимая процедура uspAddLocation

CREATE PROCEDURE [dbo].[uspAddLocation]
@Longitude FLOAT,
@Latitude FLOAT,
@PolyWKT NVARCHAR(MAX)
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

DECLARE @NewLocID int = 0
SELECT @NewLocID = MAX(LocID)+1 FROM Locations

DECLARE @NewPoly geography
SET @NewPoly = geography::STPolyFromText(@PolyWKT,4326)

INSERT INTO Locations(LocID,Name,Address,City,State,Format,Location,TradeArea)
VALUES(@NewLocID, 'New Location ' + CAST(@NewLocID As varchar(3)), '123 Main',   
 'Anywhere', 'ST', 'Food', geography::Point(@Latitude,@Longitude,4326), 
 @NewPoly)

SELECT @NewLocID AS NewLocID

END

Как видите, новый экземпляр географического объекта и переменная создаются до выражения INSERT, тогда как местоположение Point создается методом Point в INSERT. Допустим любой способ.

Код, обрабатывающий события (рис. 12), управляет кнопками previous и next, флажком отрисовки выпуклой области и закрытием формы — и этот код делает приложение законченным.

Рисунок 12 Последняя часть приложения

private void prev_Click(object sender, EventArgs e)
{
  IncDecLocation(-1);
}

private void next_Click(object sender, EventArgs e)
{
  IncDecLocation(1);
}

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
  IncDecLocation();
}

private void Map_FormClosing(object sender, FormClosingEventArgs e)
{
  db.Dispose();
  MPctrl.ActiveMap.Saved = true;
}

Просмотр и визуальное редактирование геопространственных данных SQL Server

Эта статья охватывает множество базовых понятий, но демонстрирует создание законченного приложения с поддержкой SQL Server, показывая, как передавать данные между приложением и базой данных с применением методов SQL Server Spatial и MapPoint для отображения и редактирования информации о реальном мире.

Показанные здесь принципы можно развить. Хранение локальной копии данных обеспечивает быстрое обновление карты, но окажется непрактичным в случае огромных наборов данных. В таких ситуациях данные следует извлекать по записям — в идеале с использованием механизма кеширования.

MapPoint способен выполнять некоторые полезные геопространственные операции (например, вычислять зоны по времени подъезда), но ему не хватает поддержки многих геопространственных операций, ожидаемых от полноценной геоинформационной системы (ГИС). Здесь мы используем две таких операции:STConvexHull и STDistance (опираясь на расширения SQL Server Spatial). Другие, более сложные функции, доступные в этих расширениях, позволяют, в том числе, измерять протяженность географических местоположений по долготе и широте, а также находить объединения (unions) и пересечения (intersections) полигонов. Эти функции можно было бы задействовать для создания весьма сложного приложения для управления территориями. Оно могло бы комбинировать территории или находить перекрытия, где один магазин отнимает бизнес у другого.

Аналогично можно было бы задействовать и сильные стороны MapPoint. MapPoint способен к геокодированию в автономном режиме. В нашем примере используются существующие координаты, но вместо них можно было бы применить геокодер MapPoint для поиска адресов. MapPoint также поставляется с несколькими демографическими базами данных. Эти данные можно было бы наносить на карту с магазинами, что ускорило бы анализ ситуации с магазинами, например насколько объемы продаж магазина коррелируются с численностью местного населения и средним уровнем доходов в этом районе.

Если заглянуть в ближайшее будущее, то к моменту появления следующей версии SQL Server в развитии SQL Server Spatial скорее всего произойдет резкий скачок, да и MapPoint на протяжении последних двух версий переживал настоящий ренессанс в области новых разработок и средств, так что вполне можно ожидать продолжения его развития. Более того, Entity Framework, вероятно, будет дополняться поддержкой полей новых типов, в том числе пространственных типов данных, которые должны облегчить взаимодействие между SQL Server и MapPoint. Совместно эти технологии образуют надежную и эффективную платформу для разработки картографических приложений.

Эрик Фрост (Eric Frost) — обладатель звания Microsoft MVP и разработчик бизнес-приложений со специализацией в области геоинформационных систем (ГИС) и картографии. Управляет форумом по текущим технологиям Microsoft в области картографии mapforums.com. С ним можно связаться по адресу eric.frost@mp2kmag.com.

Ричард Марсден (Richard Marsden) — обладатель звания Microsoft MVP и независимый разработчик ПО. Он продает ряд расширений MapPoint на сайте mapping-tools.com и поддерживает работу сайта GeoWeb Guru (geowebguru.com).

Выражаю благодарность за рецензирование статьи Бобу Бьюшемину (Bob Beauchemin), Эду Катибаху (Ed Katibah) и Амару Нитьянанда (Amar Nityananda).