Обновления Aspose.GIS: Редактирование объектов и геометрий и сохранение изменений в базе данных.
Обновления Aspose.GIS: Редактирование объектов и геометрий и сохранение изменений в базе данных.
Введение
В свете последних изменений в библиотеке Aspose.GIS, важно выделить некоторые из них, чтобы они не остались незамеченными. В этой статье мы обсудим новую возможность обнаружения и сохранения изменений геометрий и объектов в базе данных.
В качестве примера для демонстрации мы продолжим работать с приложением, описанным в статье “Создание карты. Скользящая карта с тайлами” и немного расширим его, добавив функциональность редактирования объектов на карте. Набор данных остается таким же, как в предыдущей статье.
Front-end
Для демонстрации возможностей модификации геометрии мы выбрали популярное расширение с открытым исходным кодом для leaflet — leaflet-geoman.
Мы добавляем эту библиотеку через файл libman.json:
Затем мы подключаем стили и скрипты к странице:
@section Styles {
<link href="~/lib/leaflet/leaflet.min.css" rel="stylesheet" />
<link href="~/lib/contagt/leaflet-geoman-free/dist/leaflet-geoman.min.css" rel="stylesheet" />
<link href="~/css/map.css" rel="stylesheet" asp-append-version="true"/>
}
@section Scripts {
<script src="~/lib/leaflet/leaflet.js"></script>
<script src="~/lib/contagt/leaflet-geoman-free/dist/leaflet-geoman.min.js"></script>
<script src="~/js/map.js" asp-append-version="true"></script>
}
Для демонстрационных целей мы ограничим возможности редактирования только зданиями. Пользователь нажимает левую кнопку мыши на карте, и если в этом месте есть здание, оно выделяется и становится доступным для редактирования. Это достигается путем наложения дополнительного слоя поверх тайлов.
Когда пользователь щелкает по карте, библиотека Leaflet вычисляет географические координаты щелчка. Мы отправляем эти координаты в бэкэнд и выполняем поиск в базе данных геометрий, пересекающихся с этой точкой щелчка. Если среди этих геометрий есть здания, мы возвращаем их.
Здания возвращаются из бэкэнда в формате GeoJSON
и добавляются на карту как отдельный слой для редактирования. Вот как мы обрабатываем щелчок:
var featuresLayer = L.featureGroup().addTo(map);
map.on('click', function (e) {
var latlng = e.latlng;
var featureFound = false;
console.log(latlng.lat + ' ' + latlng.lng);
featuresLayer.eachLayer(function (layer) {
if (layer.getBounds && layer.getBounds().contains(latlng)) {
featureFound = true;
return;
}
});
if (!featureFound) {
loadGeoJSON(latlng.lat, latlng.lng)
.then((addedFeatureLayer) => {
if (addedFeatureLayer) {
addedFeatureLayer.addTo(featuresLayer);
addedFeatureLayer.pm.enable();
console.log('Feature added.');
} else {
console.log('No feature to add.');
}
});
}
featureFound = false;
});
У нас есть постоянная группировка слоев для редактируемых геометрий, featuresLayer
, которая была добавлена на карту. Мы проверяем, был ли щелчок сделан по уже загруженной геометрии, и если нет, мы делаем запрос в бэкэнд для загрузки полигонов, представляющих здания. Загруженные слои функций добавляются к featuresLayer, и активируется режим редактирования.
Вот как выглядит функция загрузки функций и преобразования из GeoJSON
:
function loadGeoJSON(lat, lng) {
return fetch(`/features?lat=${lat}&lng=${lng}`)
.then(response => response.json())
.then(data => {
if (data && data.features && data.features.length > 0) {
return L.geoJSON(data);
} else {
return null;
}
})
.catch(error => console.error('Error loading a feature:', error));
}
После сеанса редактирования пользователь нажимает специальную кнопку Save
:
Обновите страницу и посмотрите изменения:
К сожалению, функция tiles.redraw()
не работает должным образом, поскольку ранее загруженные тайлы кэшируются, что требует принудительного обновления карты через Ctrl + F5
.
Вот обработчик нажатия кнопки сохранения:
function saveResult() {
if (featuresLayer.getLayers().length === 0) {
console.log('There are no layers to send to the server.');
return;
}
sendGeoJSONToServer()
.then(() => {
console.log('clear and update map');
featuresLayer.clearLayers();
tiles.redraw();
});
}
function sendGeoJSONToServer() {
var geojsonData = featuresLayer.toGeoJSON();
return fetch('/features', {
method: 'POST',
headers: {
'Content-Type': 'application/geo+json'
},
body: JSON.stringify(geojsonData)
})
.then(data => {
console.log('The data has been successfully sent to the server.');
})
.catch(error => {
console.error('Error when sending GeoJSON:', error);
});
}
Back-end
Здесь мы добавляем новый контроллер, FeaturesController
, где создаем обработчик для извлечения домов/объектов в соответствии с отправленными координатами.
SQL запрос выглядит следующим образом:
var latitude = lat.ToString(CultureInfo.InvariantCulture);
var longitude = lng.ToString(CultureInfo.InvariantCulture);
var query = $@"SELECT osm_id, building, name, ST_AsEWKB(way) as way
FROM public.planet_osm_polygon
WHERE ST_Intersects(way, ST_Transform(ST_SetSRID(ST_MakePoint({longitude}, {latitude}), 4326), 3857)) AND building IS NOT NULL";
Координаты преобразуются в точку, указывающую систему координат исходного запроса клиента (WGS 84), а затем переводятся в систему, в которой представлены данные базы данных (Web Mercator). Мы ищем пересечения с этой точкой для геометрий, помеченных как здания.
Выполнение запроса и отправка данных клиенту происходит аналогично тому, что мы обсуждали в предыдущей статье:
VectorLayer inputLayer;
using (var conn = new NpgsqlConnection("Host=127.0.0.1;Username=gis;Password=password;Database=Hungary"))
{
var dataSource = Drivers.PostGis
.FromQuery(query)
.GeometryField("way")
.AddAttribute("osm_id", AttributeDataType.Long)
.AddAttribute("name", AttributeDataType.String)
.AddAttribute("building", AttributeDataType.String)
.Build();
conn.Open();
inputLayer = await dataSource.ReadAsync(conn);
}
var jsonStream = new MemoryStream();
inputLayer.SaveTo(AbstractPath.FromStream(jsonStream), Drivers.GeoJson);
var result = Encoding.UTF8.GetString(jsonStream.ToArray());
return new ContentResult()
{
Content = result,
ContentType = "application/geo+json"
};
С небольшой разницей: мы сохраняем наш InMemory слой в GeoJSON в памяти как поток, затем преобразуем его в строку и отправляем клиенту.
Теперь мы переходим к сути обновлений в Aspose.GIS — сохранению изменений в базе данных. Метод Edit()
обрабатывает это. Мы читаем тело запроса, чтобы полностью загрузить его в память, и читаем его как поток:
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
// just buffer the body.
await reader.ReadToEndAsync();
Request.Body.Position = 0;
Затем мы читаем отредактированные функции в формате GeoJSON:
using (var inputLayer = VectorLayer.Open(AbstractPath.FromStream(Request.Body), Drivers.GeoJson))
Следующий шаг, из отправленного набора функций, мы извлекаем атрибуты, представляющие уникальные идентификаторы соответствующих функций в базе данных. Мы формируем запрос для заполнения специального слоя для редактирования и создаем соответствующий источник данных:
var ids = string.Join(", ", inputLayer.Select(x => x.GetValue<long>("osm_id")));
var query = $@"SELECT osm_id, building, name, ST_AsEWKB(way) as way
FROM public.planet_osm_polygon
WHERE osm_id IN ({ids});";
var dataSource = Drivers.PostGis
.FromQuery(query)
.GeometryField("way")
.AddAttribute("osm_id", AttributeDataType.Integer, System.Data.DbType.Int64)
.AddAttribute("name", AttributeDataType.String)
.AddAttribute("building", AttributeDataType.String)
.AsTrackableForChanges("public.planet_osm_polygon", "osm_id", true)
.Build();
Обратите внимание на метод конфигурации AsTrackableForChanges
. Это специальный метод, который указывает необходимость создания определенного источника данных, способного отслеживать изменения. Первый параметр указывает таблицу, в которую следует отправлять запросы об изменении. Второй указывает, какой атрибут будет считаться идентификатором для внесения изменений в базу данных. Самая интересная часть — третий параметр. При установке значения True он указывает, что слой будет отслеживать появление дубликатов в соответствии со вторым параметром и «перезаписывать» ранее загруженные функции новыми. Однако в случае результатов редактирования, то есть добавления новой функции с тем же идентификатором, будет сгенерирована команда UPDATE
в соответствии с изменениями по сравнению со старым значением. Если третий параметр установлен на false, при появлении дубликатов будет выброшено исключение, как во время инициализации, так и при редактировании.
Имена атрибутов будут использоваться в качестве имен полей в таблице для редактирования. Важно отметить важный момент относительно обнаружения изменений. Необходимо указать точный тип данных атрибута, который будет храниться в слое для отслеживания изменений, он должен быть точным в отношении вновь добавленных или измененных типов. Например, если мы добавляем новую функцию с osm_id
типа Int32
, а указанный тип атрибута в слое — Int64
, это будет рассматриваться как два разных значения, поскольку нет перегрузки метода Equal
s, которая выглядит как Int64.Equals(Int32)
. В будущих версиях это поведение будет рассмотрено и исправлено, если это возможно. Тип третьего параметра будет применяться во время сохранения данных в качестве целевого типа данных таблицы базы данных.
Затем мы подключаемся к базе данных и читаем данные из таблицы:
using var conn = new NpgsqlConnection("Host=127.0.0.1;Username=gis;Password=password;Database=Hungary");
await conn.OpenAsync();
using var transaction = await conn.BeginTransactionAsync();
var editLayer = await dataSource.ReadAsync(conn, transaction);
Важным моментом является то, что для правильной работы транзакции на уровне базы данных необходимо передать текущую транзакцию в качестве второго параметра во время операции чтения.
Затем нам нужно выполнить ряд преобразований перед отправкой изменений в базу данных:
var transformer = SpatialReferenceSystem.Wgs84.CreateTransformationTo(SpatialReferenceSystem.WebMercator);
foreach (var feature in inputLayer)
{
feature.Geometry = transformer.Transform(feature.Geometry);
((Geometry)feature.Geometry).HasZ = false;
}
foreach (var feature in inputLayer)
{
var replacingId = feature.GetValue<long>("osm_id");
var toReplaceIndex = editLayer.TakeWhile(x => x.GetValue<long>("osm_id") != replacingId).Count();
editLayer.ReplaceAt(toReplaceIndex, feature);
}
await dataSource.SubmitChangesAsync(editLayer, conn, transaction);
transaction.Commit();
Leaflet генерирует геометрии в системе координат WGS 84, однако схема базы данных требует хранения в Web Mercator. Чтобы преобразовать в систему Web Mercator, мы создаем специальный объект transformer
и используем его для преобразования.
Кроме того, leaflet заполняет третий параметр координат геометрии Z значением 0. Однако этот параметр не учитывается в нашей схеме базы данных, поэтому мы удаляем его присутствие, установив значение HasZ
равным false.
Финальным моментом является применение изменений путем замены существующей функции на ту, которая получена от клиента и имеет один и тот же osm_id. Эта операция приведет к обнаружению изменений по отношению к более старым экземплярам функции. В момент вызова SubmitChangesAsync
произойдет процесс обнаружения изменений, и команды INSERT, DELETE и UPDATE будут отправлены в базу данных в соответствии с вашими изменениями.
Спасибо за чтение до конца. Весь код будет доступен в следующем репозитории: Aspose.GIS.TilesTest