Когда я начал работать несколько лет назад, я купил Garmin Forerunner 405 . Это изящное маленькое устройство, которое отслеживает координаты GPS во время работы. После запуска устройство можно синхронизировать, загрузив данные на веб-сайт Garmin Connect . На основе отслеженного времени и GPS-координат веб-сайт Garmin Connect предоставляет вам подробный обзор вашего пробега, включая расстояние , средний темп , потерю высоты / усиление и разбивку кругов . Он также визуализирует ваш бег, накладывая отслеживаемый курс на карты Bing и / или Google. Довольно круто! Один из моих последних пробежек можно найти здесь .
Помимо простых агрегаций, таких как общее расстояние и средняя скорость, веб-сайт Garmin Connect предоставляет небольшую поддержку или вообще не помогает получить более глубокое понимание всех моих пробежек. Поскольку я часто провожу один и тот же курс, было бы интересно рассчитать мой средний темп в определенных местах . Объединяя данные всех моих курсов, я мог вычесть часто встречающиеся местоположения . Наконец, может ли быть корреляция между моим средним темпом и расстоянием от дома? Чтобы получить ответы на эти вопросы, я импортирую свои текущие данные в хранилище данных Neo4J Spatial . Neo4J Spatial расширяет базу данных Neo4J Graphс необходимыми инструментами и утилитами для хранения и запроса пространственных данных в ваших моделях графа. Для визуализации моих текущих данных я буду использовать Gephi , инструмент для визуализации и манипуляции с открытым исходным кодом, который позволяет пользователям интерактивно просматривать и исследовать графики.
1. Извлечение данных GPX
Веб-сайт Garmin Connect позволяет загружать текущие данные в различных форматах, включая KML , TCX и GPX . GPX (формат обмена GPS) — это легкий формат данных XML, который используется для обмена данными GPS (путевые точки, маршруты и треки) между приложениями и веб-службами. Ниже вы можете найти экстракт GPX, перечисляющий несколько отслеживаемых точек. Каждая из этих точек содержит местоположение GPS , высоту и соответствующую временную метку .
<trkpt lon="4.723870977759361" lat="51.075748661533"> <ele>29.799999237060547</ele> <time>2011-11-08T19:18:39.000Z</time> </trkpt> <trkpt lon="4.724105251953006" lat="51.075623352080584"> <ele>29.799999237060547</ele> <time>2011-11-08T19:18:45.000Z</time> </trkpt> <trkpt lon="4.724143054336309" lat="51.07560558244586"> <ele>29.799999237060547</ele> <time>2011-11-08T19:18:46.000Z</time> </trkpt>
На основании этих данных можно рассчитать различные показатели, в том числе темп . Для этого мы будем использовать GPSdings , библиотеку Java, которая обеспечивает необходимую функциональность для извлечения и анализа данных GPX. Начнем с чтения в файле GPX. После этого мы анализируем контент с помощью GPSdings TrackAnalyzer, который, помимо других метрик, вычисляет темп для каждой точки, которая была отслежена во время пробега. Необходимая информация хранится в первом сегменте первого трека.
// Start by reading the file and analyzing it contents Gpx gpx = GPSDings.readGPX(new FileInputStream(file)); TrackAnalyzer analyzer = new TrackAnalyzer(); analyzer.addAllTracks(gpx); // The garmin GPX running data contains only one track containing one segment Trkseg track = gpx.getTrk(0).getTrkseg(0);
2. Импорт данных GPS в Neo4J Spatial
Neo4J Spatial построен поверх Neo4J и обеспечивает поддержку пространственных данных . Как только ваши данные сохранены, могут быть выполнены пространственные операции , которые, например, позволяют искать данные в указанных регионах или на определенном расстоянии от конкретной точки интереса. Мы начнем с настройки Neo4J EmbeddedGraphDatabase . Затем мы оборачиваем его как SpatialDatabaseService , который позволяет нам создать EditableLayer . EditableLayer — это основная абстракция Neo4J, которая используется для определения коллекции геометрий . Каждый слой должен быть инициализирован определенным GeometryEncoder, который действует как адаптер для отображения графика и геометрии и наоборот. В нашем случае мы будем использовать SimplePointEncoder .
// Create the graph db graphDb = new EmbeddedGraphDatabase("var/geo"); // Wrap it as a spatial db service spatialDb = new SpatialDatabaseService(graphDb); // Create the layer to store our spatial data runningLayer = (EditableLayer) spatialDb.getOrCreateLayer("running", SimplePointEncoder.class, EditableLayerImpl.class, "lon:lat");
Добавить пространственные данные в рабочий слой очень просто. Мы начинаем с создания Координаты для каждой точки, которая анализируется GPSdings. Затем мы добавляем эту новую координату в бегущий слой. Эта операция возвращает SpatialDatabaseRecord, который под капотом является обычным узлом Neo4J . Следовательно, мы можем добавить любое свойство, которое мы хотим, к этому узлу. В нашем случае мы добавим два свойства. Одно свойство с именем speed , указывающее (средний) темп. Одно свойство, названное вхождением , указывает, сколько раз эта конкретная координата встречалась в общем наборе данных. Как только новая координата создана, мы соединяем предыдущий узел с вновь созданным узлом через NEXTтип отношений. Следовательно, наш граф является перечислением встреченных координат, связанных через СЛЕДУЮЩИЕ ребра.
// Create a new coordinate for this point Coordinate to = new Coordinate(track.getTrkpt(i).getLon().doubleValue(),track.getTrkpt(i).getLat().doubleValue()); // Add the new coordinate torecord = runningLayer.add(runningLayer.getGeometryFactory().createPoint(to)); // Set the data accordingly torecord.setProperty("speed", analyzer.getHorizontalSpeed(track.getTrkpt(i).getTime())); torecord.setProperty("occurences", 1); // Add relationship Relationship next = fromrecord.getGeomNode().createRelationshipTo(torecord.getGeomNode(), RelTypes.NEXT);
Если координата встречается несколько раз, мы пересчитываем среднюю скорость и увеличиваем количество встреч .
// Recalculate average speed double previousspeed = (Double)torecord.getProperty("speed"); int previousoccurences = (Integer)torecord.getProperty("occurences"); double currentspeed = analyzer.getHorizontalSpeed(track.getTrkpt(i).getTime()); double denormalizespeed = previousspeed * previousoccurences; double newspeed = ((denormalizespeed + currentspeed) / (previousoccurences + 1)); // Update the data accordingly torecord.setProperty("speed",newspeed); torecord.setProperty("occurences",previousoccurences+1);
К сожалению, шансы встретить уже существующую координату малы, так как координаты в файле GPX имеют 15-значную точность справа от десятичной точки. Вместо того, чтобы пытаться округлить эти координаты самостоятельно, мы будем использовать Neo4J Spatial Querying API . Простой ближайший сосед — поиск, ограниченный 20 метрами, позволяет нам находить совпадающие координаты. (Я выбираю 20 метров, так как 20 немного выше среднего расстояния между двумя координатами). Если мы найдем координату в пределах этого 20-метрового диапазона, мы будем использовать ее повторно . В противном случае мы просто создаем новую координату . Полный алгоритм импорта нескольких наборов данных GPX можно найти ниже.
// Import the data from a GPX file. Boolean indicates whether data has been imported before public void addData(File file, boolean firsttime) throws IOException, FunctionEvaluationException { // Start by reading the file and analyzing it contents Gpx gpx = GPSDings.readGPX(new FileInputStream(file)); TrackAnalyzer analyzer = new TrackAnalyzer(); analyzer.addAllTracks(gpx); // The garmin GPX running data contains only one track containing one segment Trkseg track = gpx.getTrk(0).getTrkseg(0); // Start a new transaction Transaction tx = graphDb.beginTx(); // Contains the record that was added previously (in order to create a relation between the new and the previous node) SpatialDatabaseRecord fromrecord = null; // Iterate all points for (int i = 0; i < track.getTrkptCount(); i++) { // Create a new coordinate for this point Coordinate to = new Coordinate(track.getTrkpt(i).getLon().doubleValue(),track.getTrkpt(i).getLat().doubleValue()); // Check whether we can find a node from which is located within a distance of 20 meters List<GeoPipeFlow> closests = GeoPipeline.startNearestNeighborLatLonSearch(runningLayer, to, 0.02).sort("OrthodromicDistance").getMin("OrthodromicDistance").toList(); SpatialDatabaseRecord torecord = null; // If first time, we add all nodes. Otherwise, we check whether we find a node that is close enough to the current location if (!firsttime && (closests.size() == 1)) { // Retrieve the node System.out.println("Using existing: " + closests.get(0).getProperty("OrthodromicDistance")); torecord = closests.get(0).getRecord(); // Recalculate average speed double previousspeed = (Double)torecord.getProperty("speed"); int previousoccurences = (Integer)torecord.getProperty("occurences"); double currentspeed = analyzer.getHorizontalSpeed(track.getTrkpt(i).getTime()); double denormalizespeed = previousspeed * previousoccurences; double newspeed = ((denormalizespeed + currentspeed) / (previousoccurences + 1)); // Update the data accordingly torecord.setProperty("speed",newspeed); torecord.setProperty("occurences",previousoccurences+1); } else { // New node, add it torecord = runningLayer.add(runningLayer.getGeometryFactory().createPoint(to)); // Set the data accordingly torecord.setProperty("speed", analyzer.getHorizontalSpeed(track.getTrkpt(i).getTime())); torecord.setProperty("occurences", 1); } // If a previous node is available (and they are not identical), add a directed relationship between both if (fromrecord != null && (!fromrecord.equals(torecord))) { Relationship next = fromrecord.getGeomNode().createRelationshipTo(torecord.getGeomNode(), RelTypes.NEXT); } // Previous record is put on new record fromrecord = torecord; } // Commit transaction tx.success(); tx.finish(); }
3. Визуализация бегущих данных
Используя API пространственных запросов Neo4J , мы можем получить набор координат, которые удовлетворяют определенному условию. Однако координаты несколько абстрактны для интерпретации. Вместо этого мы будем использовать отличный инструмент визуализации и исследования Gephi Graph. Установив плагин Gephi Neo4j , мы можем загрузить и изучить графики, которые хранятся в (Spatial) датасторе Neo4j. Давайте начнем с импорта нашего набора данных в Gephi.
Отображаемый график содержит другие типы узлов и ребер (например, информацию об уровне слоя и RTree ), в дополнение к координатам и СЛЕДУЮЩИМ ребрам, которые мы добавили сами. Давайте избавимся от них, отфильтровав наш граф по следующему типу отношений.
Остается только половина краев … Однако мы все равно не получим нового понимания этого беспорядка. Давайте разместим наш график с помощью плагина Gephi GeoLayout . Этот Layouter принимает геокодированные графики в качестве входных данных и строит графики в соответствии с геокодированными атрибутами. Обязательно увеличьте масштабирование, так как наши координаты расположены близко друг к другу. Прохладно! Эта точка зрения четко описывает курсы, которые я провожу.
Давайте визуализируем координаты, которые часто встречались во время 4 прогонов, которые импортируются в хранилище данных Neo4J Spatial. Для этого мы будем использовать свойство узла InDegree , которое указывает количество входящих ребер для каждой координаты. Мы оцениваем вес узла (т.е. размер узла) через это свойство. Следовательно, часто встречающиеся узлы будут отображаться больше. В моем случае часто встречающиеся координаты находятся вокруг того места, где я живу (и, следовательно, начинаю бегать) и на перекрестках улиц.
Давайте сделаем один заключительный анализ, а именно визуализацию, которая иллюстрирует средний темп на протяжении всех запусков . Для этого, мы занимаем как вес узла и цвет узла через скорость свойства. Следовательно, координаты с высоким средним темпом окрашены в зеленый цвет и отображаются больше. Координаты с низким средним темпом окрашены в красный цвет и отображаются меньше. В мгновение ока я теперь могу интерпретировать свой средний темп, учитывая мой общий набор бегущих данных!
4. Вывод
В этой статье описывается использование хранилища данных Neo4J Spatial и Gephi для анализа текущих данных Garmin. Как всегда, полный исходный код можно найти в общедоступном GitHub-хранилище Datablend . Любые идеи для других типов анализа, которые могут быть выполнены на наборе данных?