Implemented (partially) Voronoi algorithm for zone-dividing in Seville map. Also refactored some things in frontend. Modified hardware firmware for conditional compilation for both SENSOR and ACTUATOR type boards.
This commit is contained in:
@@ -73,6 +73,27 @@
|
||||
<version>1.5.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.locationtech.jts/jts-core -->
|
||||
<dependency>
|
||||
<groupId>org.locationtech.jts</groupId>
|
||||
<artifactId>jts-core</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.locationtech.jts.io/jts-io-common -->
|
||||
<dependency>
|
||||
<groupId>org.locationtech.jts.io</groupId>
|
||||
<artifactId>jts-io-common</artifactId>
|
||||
<version>1.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>2.19.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package net.miarma.contaminus.common;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.locationtech.jts.io.geojson.GeoJsonReader;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
public class VoronoiZoneDetector {
|
||||
|
||||
private static class Zone {
|
||||
Polygon polygon;
|
||||
String actuatorId;
|
||||
|
||||
public Zone(Polygon polygon, String actuatorId) {
|
||||
this.polygon = polygon;
|
||||
this.actuatorId = actuatorId;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Zone> zones = new ArrayList<>();
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public VoronoiZoneDetector(String geojsonUrl, boolean isUrl) throws Exception {
|
||||
String geojsonStr;
|
||||
|
||||
if(isUrl) {
|
||||
try(InputStream is = URL.of(URI.create(geojsonUrl), null).openStream()) {
|
||||
geojsonStr = new String(is.readAllBytes());
|
||||
}
|
||||
} else {
|
||||
geojsonStr = Files.readString(new File(geojsonUrl).toPath());
|
||||
}
|
||||
|
||||
JsonObject root = JsonParser.parseString(geojsonStr).getAsJsonObject();
|
||||
JsonArray features = root.getAsJsonArray("features");
|
||||
GeoJsonReader reader = new GeoJsonReader(geometryFactory);
|
||||
|
||||
for (int i = 0; i < features.size(); i++) {
|
||||
JsonObject feature = features.get(i).getAsJsonObject();
|
||||
|
||||
String actuatorId = feature
|
||||
.getAsJsonObject("properties")
|
||||
.get("actuatorId")
|
||||
.getAsString();
|
||||
|
||||
JsonObject geometryJson = feature.getAsJsonObject("geometry");
|
||||
String geometryStr = gson.toJson(geometryJson);
|
||||
|
||||
Geometry geometry = reader.read(geometryStr);
|
||||
|
||||
if (geometry instanceof Polygon polygon) {
|
||||
zones.add(new Zone(polygon, actuatorId));
|
||||
} else {
|
||||
Constants.LOGGER.error("⚠️ Geometría ignorada: no es un polígono");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getZoneForPoint(double lon, double lat) {
|
||||
Point p = geometryFactory.createPoint(new Coordinate(lon, lat));
|
||||
|
||||
for (Zone z : zones) {
|
||||
if (z.polygon.covers(p)) {
|
||||
return z.actuatorId;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // no está dentro de ninguna zona
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
VoronoiZoneDetector detector = new VoronoiZoneDetector("https://miarma.net/files/voronoi_sevilla_geovoronoi.geojson", true);
|
||||
|
||||
double lon = -5.9752;
|
||||
double lat = 37.3887;
|
||||
|
||||
String actuatorId = detector.getZoneForPoint(lon, lat);
|
||||
if (actuatorId != null) {
|
||||
System.out.println("📍 El punto pertenece al actuator: " + actuatorId);
|
||||
} else {
|
||||
System.out.println("🚫 El punto no pertenece a ninguna zona");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,12 @@ public class LogicLayerAPIVerticle extends AbstractVerticle {
|
||||
WebClientOptions options = new WebClientOptions()
|
||||
.setUserAgent("ContaminUS");
|
||||
this.restClient = new RestClientUtil(WebClient.create(Vertx.vertx(), options));
|
||||
this.mqttClient = MqttClient.create(Vertx.vertx(), new MqttClientOptions().setAutoKeepAlive(true));
|
||||
this.mqttClient = MqttClient.create(Vertx.vertx(),
|
||||
new MqttClientOptions()
|
||||
.setAutoKeepAlive(true)
|
||||
.setUsername("contaminus")
|
||||
.setPassword("contaminus")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,11 +75,25 @@ public class LogicLayerAPIVerticle extends AbstractVerticle {
|
||||
router.route(HttpMethod.GET, Constants.HISTORY).handler(this::getDeviceHistory);
|
||||
router.route(HttpMethod.GET, Constants.SENSOR_VALUES).handler(this::getSensorValues);
|
||||
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getLogicApiPort(), configManager.getHost());
|
||||
|
||||
startPromise.complete();
|
||||
mqttClient.connect(1883, "localhost", ar -> {
|
||||
if (ar.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 MQTT client connected");
|
||||
vertx.createHttpServer()
|
||||
.requestHandler(router)
|
||||
.listen(configManager.getLogicApiPort(), configManager.getHost(), http -> {
|
||||
if (http.succeeded()) {
|
||||
Constants.LOGGER.info("🟢 HTTP server started on port " + configManager.getLogicApiPort());
|
||||
startPromise.complete();
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 HTTP server failed to start: " + http.cause());
|
||||
startPromise.fail(http.cause());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Constants.LOGGER.error("🔴 MQTT client connection failed: " + ar.cause());
|
||||
startPromise.fail(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getDeviceLatestValues(RoutingContext context) {
|
||||
@@ -152,27 +171,18 @@ public class LogicLayerAPIVerticle extends AbstractVerticle {
|
||||
WeatherValue weatherValue = gson.fromJson(weather.toString(), WeatherValue.class);
|
||||
COValue coValue = gson.fromJson(co.toString(), COValue.class);
|
||||
|
||||
// MQTT publish =============================
|
||||
float coAmount = coValue.getValue();
|
||||
Constants.LOGGER.info("CO amount received: " + coAmount);
|
||||
String topic = buildTopic(Integer.parseInt(groupId), deviceId, "matrix");
|
||||
if (coAmount >= 80.0f) {
|
||||
mqttClient.connect(1883, "miarma.net", ar -> {
|
||||
if(ar.succeeded()) {
|
||||
Constants.LOGGER.info("Connected to MQTT broker");
|
||||
mqttClient.publish(topic, Buffer.buffer("ECO"), MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
} else {
|
||||
Constants.LOGGER.error("Error connecting to MQTT broker: " + ar.cause().getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mqttClient.connect(1883, "miarma.net", ar -> {
|
||||
if(ar.succeeded()) {
|
||||
Constants.LOGGER.info("Connected to MQTT broker");
|
||||
mqttClient.publish(topic, Buffer.buffer("GAS"), MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
} else {
|
||||
Constants.LOGGER.error("Error connecting to MQTT broker: " + ar.cause().getMessage());
|
||||
}
|
||||
});
|
||||
Constants.LOGGER.info("Topic: " + topic);
|
||||
if (mqttClient.isConnected()) {
|
||||
Constants.LOGGER.info("🟢 Publishing to MQTT topic: " + topic + " with value: " + coAmount);
|
||||
mqttClient.publish(topic, Buffer.buffer(coAmount >= 80.0f ? "ECO" : "GAS"),
|
||||
MqttQoS.AT_LEAST_ONCE, false, false);
|
||||
Constants.LOGGER.info("🟢 Message published to MQTT topic: " + topic);
|
||||
}
|
||||
// ============================================
|
||||
|
||||
gpsValue.setDeviceId(deviceId);
|
||||
weatherValue.setDeviceId(deviceId);
|
||||
|
||||
@@ -17,7 +17,7 @@ import net.miarma.contaminus.common.Constants;
|
||||
public class MainVerticle extends AbstractVerticle {
|
||||
private ConfigManager configManager;
|
||||
|
||||
public static void main(String[] args) {
|
||||
public static void main(String[] args) {
|
||||
Launcher.executeCommand("run", MainVerticle.class.getName());
|
||||
}
|
||||
|
||||
|
||||
18
backend/src/main/resources/coords.txt
Normal file
18
backend/src/main/resources/coords.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
Bellavista: 37.32533897043053, -5.968045749476732
|
||||
Bermejales: 37.3490769843888, -5.976422439823011
|
||||
Los Remedios: 37.37515235961667, -6.000317870442392
|
||||
Tiro de Línea: 37.36841756821152, -5.97598991091761
|
||||
San Bernardo: 37.3789653804891, -5.987731412877974
|
||||
Nervión: 37.38310913923199, -5.97320443871298
|
||||
Cerro del Águila: 37.3733690603403, -5.960390435428303
|
||||
Polígono Sur: 37.36134485686387, -5.965857306623599
|
||||
Amate: 37.381087013389035, -5.953545655643191
|
||||
Sevilla Este: 37.39944168632523, -5.925092000775878
|
||||
Valdezorras: 37.429014427259844, -5.927831833065477
|
||||
Pino Montano: 37.423390190328654, -5.973241522877093
|
||||
Macarena: 37.40735604254458, -5.980276144202576
|
||||
Centro: 37.39277256619283, -5.994765524658572
|
||||
Santa Justa: 37.394092596421686, -5.962662563452358
|
||||
Santa Clara: 37.40050556650672, -5.954710495719027
|
||||
Triana: 37.380789093980844, -6.008393542942082
|
||||
Cartuja: 37.403427709335816, -6.007627600183472
|
||||
70
backend/src/main/resources/voronoi.py
Normal file
70
backend/src/main/resources/voronoi.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import geopandas as gpd # esto es necesario para leer el GeoJSON
|
||||
import matplotlib.pyplot as plt # esto es necesario para graficar
|
||||
import numpy as np # esto es necesario para manejar los arrays
|
||||
import json # esto es necesario para guardar el GeoJSON
|
||||
from shapely.geometry import Polygon # esto es necesario para crear la caja de recorte
|
||||
from geovoronoi import voronoi_regions_from_coords # esto es necesario para crear el Voronoi
|
||||
|
||||
# se cargan los puntos (coordenadas) de los actuadores
|
||||
# en formato GeoJSON
|
||||
points_gdf = gpd.read_file("sevilla.geojson")
|
||||
coords = np.array([[geom.x, geom.y] for geom in points_gdf.geometry])
|
||||
|
||||
# esto es una "caja" alrededor de Sevilla para recortar el Voronoi
|
||||
# para que las regiones no acotadas no sean infinitas
|
||||
seville_boundary = Polygon([
|
||||
(-6.10, 37.30),
|
||||
(-5.85, 37.30),
|
||||
(-5.85, 37.45),
|
||||
(-6.10, 37.45)
|
||||
])
|
||||
area_gdf = gpd.GeoDataFrame(geometry=[seville_boundary], crs="EPSG:4326")
|
||||
|
||||
# se genera el Voronoi con las coordenadas de los actuadores
|
||||
# y la caja de recorte (unión de todo)
|
||||
region_polys, region_pts = voronoi_regions_from_coords(coords, area_gdf.union_all())
|
||||
|
||||
# dibuja con matplotlib
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
|
||||
for poly in region_polys.values():
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.3, edgecolor='black')
|
||||
|
||||
ax.plot(coords[:, 0], coords[:, 1], 'ro')
|
||||
for idx, coord in enumerate(coords):
|
||||
ax.text(coord[0], coord[1], points_gdf.iloc[idx]["name"], fontsize=8, ha='center')
|
||||
|
||||
ax.set_title("Zonas Voronoi por Actuator (GeoVoronoi)")
|
||||
plt.axis("equal")
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# Guardar GeoJSON
|
||||
features = []
|
||||
for idx, region in region_polys.items():
|
||||
point_index = region_pts[idx] # índice del punto original
|
||||
name = points_gdf.loc[point_index, "name"]
|
||||
if isinstance(name, gpd.GeoSeries):
|
||||
name = name.iloc[0] # o el que tú quieras
|
||||
name = str(name)
|
||||
|
||||
feature = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"actuatorId": name
|
||||
},
|
||||
"geometry": json.loads(gpd.GeoSeries([region]).to_json())["features"][0]["geometry"]
|
||||
}
|
||||
features.append(feature)
|
||||
|
||||
geojson_output = {
|
||||
"type": "FeatureCollection",
|
||||
"features": features
|
||||
}
|
||||
|
||||
with open("voronoi_sevilla_geovoronoi.geojson", "w") as f:
|
||||
json.dump(geojson_output, f, indent=2)
|
||||
|
||||
print("✅ GeoJSON guardado como 'voronoi_sevilla_geovoronoi.geojson'")
|
||||
Reference in New Issue
Block a user