diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..924124d
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
index b5ecc69..2c8697e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
# DrawIO backup files
-**/*.bkp
\ No newline at end of file
+**/*.bkp
+/bin/
+/target/
+/data/
+*.kml
\ No newline at end of file
diff --git a/.project b/.project
new file mode 100644
index 0000000..9e96a03
--- /dev/null
+++ b/.project
@@ -0,0 +1,23 @@
+
+
+ PLP
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.m2e.core.maven2Nature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..623e652
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,8 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
+org.eclipse.jdt.core.compiler.compliance=21
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=21
diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6fec576
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,71 @@
+
+ 4.0.0
+ PLP
+ PLP
+ 1.0.0
+
+
+ 2.0.16
+
+
+
+
+ JOSM
+ JOSM
+ https://josm.openstreetmap.de/nexus/content/repositories/releases/
+
+
+
+
+
+ com.uber
+ h3
+ 4.1.1
+
+
+ uk.m0nom
+ javaapiforkml
+ 3.0.10
+
+
+ org.reflections
+ reflections
+ 0.10.2
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+ org.locationtech.jts
+ jts-core
+ 1.20.0
+
+
+ org.openstreetmap.jmapviewer
+ jmapviewer
+ 2.22
+
+
+
+
+ src
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+ 21
+
+
+
+
+
\ No newline at end of file
diff --git a/src/plp/Config.java b/src/plp/Config.java
new file mode 100644
index 0000000..081b2d6
--- /dev/null
+++ b/src/plp/Config.java
@@ -0,0 +1,5 @@
+package plp;
+
+public class Config {
+ public static final int H3_RESOLUTION = 9; // Shared configuration variable for resolution
+}
diff --git a/src/plp/SystemRunner.java b/src/plp/SystemRunner.java
new file mode 100644
index 0000000..773ce4f
--- /dev/null
+++ b/src/plp/SystemRunner.java
@@ -0,0 +1,51 @@
+package plp;
+
+import java.util.List;
+
+import plp.filter.DataFilter;
+import plp.filters.*;
+import plp.location.LocationCell;
+import plp.operator.LogicalOperator;
+import plp.output.KMLGenerator;
+
+public class SystemRunner {
+ public static void main(String[] args) {
+
+ // Create Filters
+ LightPollutionFilter lightPollutionFilter = new LightPollutionFilter();
+ lightPollutionFilter.setRequirements(21.65); // Minimum SQM value
+
+ LightPollutionFilter lightPollutionFilterNot = new LightPollutionFilter();
+ lightPollutionFilterNot.setRequirements(21.6); // Minimum SQM value
+
+ BoundingBoxFilter boundingBoxFilter = new BoundingBoxFilter();
+ boundingBoxFilter.setRequirements(new double[]{33.5, 34.2, -116.5, -115.0}); // Desert bounding box
+
+ // Use DataFilter
+ DataFilter dataFilter = new DataFilter(boundingBoxFilter);
+
+ // SQM less than 17.9
+ OperatorFilter notFilter = new OperatorFilter();
+ notFilter.setRequirements(LogicalOperator.NOT);
+ notFilter.addFilter(lightPollutionFilterNot);
+
+ // SQM less than 17.9 or greater than 18.1
+ OperatorFilter orFilter = new OperatorFilter();
+ orFilter.setRequirements(LogicalOperator.OR);
+ orFilter.addFilter(notFilter);
+ orFilter.addFilter(lightPollutionFilter);
+ dataFilter.addFilter(orFilter);
+
+
+ // Apply Filters
+ List filteredLocations = dataFilter.filterLocations();
+
+ // Print Results
+ System.out.println("Filtered Locations: " + filteredLocations);
+
+ // Generate KML file with hexagon boundaries
+ String kmlFileName = "filtered_hexagons.kml";
+ KMLGenerator.generateKML(filteredLocations, kmlFileName);
+
+ }
+}
diff --git a/src/plp/filter/DataFilter.java b/src/plp/filter/DataFilter.java
new file mode 100644
index 0000000..416c825
--- /dev/null
+++ b/src/plp/filter/DataFilter.java
@@ -0,0 +1,28 @@
+package plp.filter;
+
+import java.util.List;
+
+
+import plp.filters.BoundingBoxFilter;
+import plp.location.LocationCell;
+
+public class DataFilter {
+ private final FilterManager filterManager = new FilterManager();
+ private final List allCells;
+
+ public DataFilter(InitialFilter initialFilter) {
+ allCells = initialFilter.getValidCells();
+ if (allCells.isEmpty()) {
+ throw new IllegalArgumentException("Zero cells in the initialFilter: " + initialFilter.getClass().getSimpleName());
+ }
+ }
+
+
+ public void addFilter(Filter filter) {
+ filterManager.addFilter(filter);
+ }
+
+ public List filterLocations() {
+ return filterManager.applyFilters(allCells);
+ }
+}
\ No newline at end of file
diff --git a/src/plp/filter/Filter.java b/src/plp/filter/Filter.java
new file mode 100644
index 0000000..8fddcec
--- /dev/null
+++ b/src/plp/filter/Filter.java
@@ -0,0 +1,73 @@
+package plp.filter;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import plp.location.LocationCell;
+
+public interface Filter {
+
+ /**
+ * Set the requirements from the CLI or other source.
+ * Must detect the wanted Object type and handle accordingly.
+ * @param requirements The filter's requirements in it's wanted type.
+ * @throws IllegalArgumentException If object is of wrong type or arguments are invalid
+ */
+ void setRequirements(Object requirements) throws IllegalArgumentException; // Main Handling
+
+ /**
+ * A String representation of the Filter's currently inputed requirements
+ * @return A user-friendly string
+ */
+ String getRequirements();
+
+ /**
+ * Set the initial locations to filter upon
+ * @param Previous {@link plp.location.LocationCell LocationCells} to work with
+ */
+ void setLocations(List locations);
+
+ /**
+ * The action of filtering the locations.
+ * Should handle loading data in and caching, if necessary.
+ * @return All matching {@link plp.location.LocationCell LocationCells}
+ */
+ List process(); // Action of filtering the locations
+
+ /**
+ * Accept requirements from the Parameter Panel that this filter provides
+ * @param modifiedParameterPanel The panel from {@link #getParameterPanel(int, int) getParameterPanel}, modified with the user's input.
+ * @throws IllegalArgumentException If arguments are invalid.
+ */
+ default void setRequirements(JPanel modifiedParameterPanel) throws IllegalArgumentException { // Handle from UI
+ JTextField[] fields = (JTextField[]) modifiedParameterPanel.getClientProperty("fields"); // Get the first component (the filter's parameter panel), Extract input fields
+
+ try {
+ String[] inputValues = new String[fields.length];
+ for (int i = 0; i < fields.length; i++) {
+ // Explicitly focus out of each field to ensure the latest value is captured
+ fields[i].transferFocus();
+ inputValues[i] = fields[i].getText();
+ }
+
+ System.out.println(Arrays.toString(inputValues));
+ setRequirements(fields); // Set the requirements dynamically
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(modifiedParameterPanel, "Invalid input: " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Construct a JPanel for the UI so that the user can input the requirements for this filter.
+ * @return A panel with fields, such as {@link javax.swing.JTextField JTextFields}
+ */
+ default JPanel getParameterPanel() {
+ JPanel panel = new JPanel();
+ panel.putClientProperty("fields", new JTextField[]{});
+ return panel;
+ }
+}
diff --git a/src/plp/filter/FilterManager.java b/src/plp/filter/FilterManager.java
new file mode 100644
index 0000000..cfebb67
--- /dev/null
+++ b/src/plp/filter/FilterManager.java
@@ -0,0 +1,27 @@
+package plp.filter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import plp.location.LocationCell;
+
+public class FilterManager {
+ private final List filters = new ArrayList<>();
+
+ public void addFilter(Filter filter) {
+ filters.add(filter);
+ }
+
+ public List applyFilters(List locations) {
+ List filteredLocations = locations;
+ System.out.println("Inital bounds: " + filteredLocations.size());
+
+ for (Filter filter : filters) {
+ filter.setLocations(filteredLocations);
+ filteredLocations = filter.process();
+ System.out.println("After " + filter.getClass().getSimpleName() + ": " + filteredLocations.size());
+ }
+
+ return filteredLocations;
+ }
+}
diff --git a/src/plp/filter/InitialFilter.java b/src/plp/filter/InitialFilter.java
new file mode 100644
index 0000000..bf3f45a
--- /dev/null
+++ b/src/plp/filter/InitialFilter.java
@@ -0,0 +1,18 @@
+package plp.filter;
+
+import java.util.List;
+
+import plp.location.LocationCell;
+
+/*
+ * This type of filter can be used at the beginning of a sequence to get points from nothing.
+ */
+public interface InitialFilter extends Filter {
+
+ /**
+ * Get the valid cells of the filter.
+ * Must not require {@link plp.filter.Filter#setLocations(List)} to be called first.
+ * @return List of Cell h3 indexes
+ */
+ List getValidCells();
+}
diff --git a/src/plp/filters/BoundingBoxFilter.java b/src/plp/filters/BoundingBoxFilter.java
new file mode 100644
index 0000000..7cc76f0
--- /dev/null
+++ b/src/plp/filters/BoundingBoxFilter.java
@@ -0,0 +1,159 @@
+package plp.filters;
+
+import java.awt.GridLayout;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import com.uber.h3core.H3Core;
+import com.uber.h3core.util.LatLng;
+
+import plp.Config;
+import plp.filter.InitialFilter;
+import plp.location.LocationCell;
+
+public class BoundingBoxFilter implements InitialFilter {
+ private double minLatitude;
+ private double maxLatitude;
+ private double minLongitude;
+ private double maxLongitude;
+ private List validCells;
+ private List locations;
+ private H3Core h3;
+
+ public BoundingBoxFilter() {
+ try {
+ h3 = H3Core.newInstance();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to initialize H3 library", e);
+ }
+ }
+
+ @Override
+ public void setRequirements(JPanel modifiedParameterPanel) {
+ JTextField[] fields = (JTextField[]) modifiedParameterPanel.getClientProperty("fields"); // Get the first component (the filter's parameter panel), Extract input fields
+
+ try {
+ String[] inputValues = new String[fields.length];
+ for (int i = 0; i < fields.length; i++) {
+ // Explicitly focus out of each field to ensure the latest value is captured
+ fields[i].transferFocus();
+ inputValues[i] = fields[i].getText();
+ }
+
+ System.out.println(Arrays.toString(inputValues));
+ double minLat = Double.parseDouble(inputValues[0]);
+ double maxLat = Double.parseDouble(inputValues[1]);
+ double minLon = Double.parseDouble(inputValues[2]);
+ double maxLon = Double.parseDouble(inputValues[3]);
+ setRequirements(new double[]{minLat, maxLat, minLon, maxLon});
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(modifiedParameterPanel, "Invalid input: " + ex.getMessage());
+ }
+ }
+
+ @Override
+ public void setRequirements(Object requirements) throws IllegalArgumentException {
+ if (requirements instanceof double[]) {
+ double[] bounds = (double[]) requirements;
+ if (bounds.length == 4) {
+ validateBounds(bounds);
+ this.minLatitude = bounds[0];
+ this.maxLatitude = bounds[1];
+ this.minLongitude = bounds[2];
+ this.maxLongitude = bounds[3];
+
+ validCells = h3.polygonToCells(Arrays.asList(
+ new LatLng(minLatitude, minLongitude),
+ new LatLng(minLatitude, maxLongitude),
+ new LatLng(maxLatitude, maxLongitude),
+ new LatLng(maxLatitude, minLongitude)),
+ null, Config.H3_RESOLUTION);
+ } else {
+ throw new IllegalArgumentException("Bounding box requires exactly 4 values: [minLat, maxLat, minLon, maxLon]");
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid requirement type for BoundingBoxFilter");
+ }
+ }
+
+ @Override
+ public String getRequirements() {
+ // TODO Auto-generated method stub
+ return String.valueOf(minLatitude) + " -> " + String.valueOf(maxLatitude) + ", " + String.valueOf(minLongitude) + " -> " + String.valueOf(maxLongitude);
+ }
+
+ private void validateBounds(double[] bounds) {
+ double minLat = bounds[0];
+ double maxLat = bounds[1];
+ double minLon = bounds[2];
+ double maxLon = bounds[3];
+
+ if (minLat < -90 || maxLat > 90) {
+ throw new IllegalArgumentException("Latitude values must be between -90 and 90.");
+ }
+
+ if (minLat > maxLat) {
+ throw new IllegalArgumentException("Latitude minLat must be <= maxLat.");
+ }
+
+ if (minLon < -180 || maxLon > 180) {
+ throw new IllegalArgumentException("Longitude values must be between -180 and 180.");
+ }
+
+ if (minLon > maxLon) {
+ throw new IllegalArgumentException("Longitude minLon must be <= maxLon.");
+ }
+ }
+
+ @Override
+ public void setLocations(List locations) {
+ this.locations = locations;
+ }
+
+ @Override
+ public List process() {
+ return locations.stream()
+ .filter(cell -> validCells.contains(cell.getH3Index()))
+ .toList();
+ }
+
+ /**
+ * Get the valid cells of the bounding box
+ * @return List of Cell h3 indexes
+ */
+ public List getValidCells() {
+ return validCells.stream()
+ .map(LocationCell::new)
+ .toList();
+ }
+
+ @Override
+ public JPanel getParameterPanel() {
+ JPanel panel = new JPanel(new GridLayout(0, 2));
+
+ panel.add(new JLabel("Min Latitude:"));
+ JTextField minLatField = new JTextField("33.5");
+ panel.add(minLatField);
+
+ panel.add(new JLabel("Max Latitude:"));
+ JTextField maxLatField = new JTextField("34.2");
+ panel.add(maxLatField);
+
+ panel.add(new JLabel("Min Longitude:"));
+ JTextField minLonField = new JTextField("-116.5");
+ panel.add(minLonField);
+
+ panel.add(new JLabel("Max Longitude:"));
+ JTextField maxLonField = new JTextField("-115");
+ panel.add(maxLonField);
+
+ panel.putClientProperty("fields", new JTextField[]{minLatField, maxLatField, minLonField, maxLonField});
+ return panel;
+ }
+}
diff --git a/src/plp/filters/BoundingEllipseFilter.java b/src/plp/filters/BoundingEllipseFilter.java
new file mode 100644
index 0000000..1611e57
--- /dev/null
+++ b/src/plp/filters/BoundingEllipseFilter.java
@@ -0,0 +1,491 @@
+package plp.filters;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+import org.openstreetmap.gui.jmapviewer.JMapViewer;
+import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
+import org.openstreetmap.gui.jmapviewer.MapPolygonImpl;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
+
+import com.uber.h3core.H3Core;
+import com.uber.h3core.util.LatLng;
+
+import plp.Config;
+import plp.filter.InitialFilter;
+import plp.location.LocationCell;
+
+public class BoundingEllipseFilter implements InitialFilter {
+ private LatLng center;
+ private double majorAxis;
+ private double minorAxis;
+ private double rotation;
+ private List validCells;
+ private List locations;
+ private H3Core h3;
+
+ public BoundingEllipseFilter() {
+ try {
+ h3 = H3Core.newInstance();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to initialize H3 library", e);
+ }
+ center = null; // Center is not initialized until user interaction
+ majorAxis = 1.0; // Default major axis length in degrees
+ minorAxis = 1.0; // Default minor axis length in degrees
+ rotation = 0.0; // Default rotation angle in degrees
+ }
+
+ @Override
+ public void setRequirements(JPanel modifiedParameterPanel) {
+ MapPanel mapPanel = (MapPanel) modifiedParameterPanel.getClientProperty("mapPanel");
+ if (mapPanel == null) {
+ throw new IllegalArgumentException("Parameter panel does not contain the required map panel.");
+ }
+
+ center = mapPanel.getCenter();
+ majorAxis = mapPanel.getMajorAxis();
+ minorAxis = mapPanel.getMinorAxis();
+ rotation = mapPanel.getRotation();
+
+ // Generate H3 indexes within the ellipse boundary
+ validCells = h3.polygonToCells(getEllipseBoundary(), null, Config.H3_RESOLUTION);
+ }
+
+ @Override
+ public void setRequirements(Object requirements) throws IllegalArgumentException {
+ if (requirements instanceof EllipseRequirements req) {
+ validateEllipseParameters(req.center, req.majorAxis, req.minorAxis, req.rotation);
+ this.center = req.center;
+ this.majorAxis = req.majorAxis;
+ this.minorAxis = req.minorAxis;
+ this.rotation = req.rotation;
+
+ // Generate H3 indexes within the ellipse boundary
+ validCells = h3.polygonToCells(getEllipseBoundary(), null, Config.H3_RESOLUTION);
+ } else {
+ throw new IllegalArgumentException("Invalid requirement type for BoundingEllipseFilter");
+ }
+ }
+
+ private void validateEllipseParameters(LatLng center, double majorAxis, double minorAxis, double rotation) {
+ if (center == null) {
+ throw new IllegalArgumentException("Center point cannot be null.");
+ }
+ if (majorAxis <= 0 || minorAxis <= 0) {
+ throw new IllegalArgumentException("Major and minor axes must be positive values.");
+ }
+ if (rotation < 0 || rotation >= 360) {
+ throw new IllegalArgumentException("Rotation must be between 0 and 360 degrees.");
+ }
+ }
+
+ @Override
+ public String getRequirements() {
+ if (center == null) {
+ return "Ellipse not initialized";
+ }
+ return String.format("Center: %s, Major Axis: %.2f, Minor Axis: %.2f, Rotation: %.2f", center, majorAxis, minorAxis, rotation);
+ }
+
+ @Override
+ public void setLocations(List locations) {
+ this.locations = locations;
+ }
+
+ @Override
+ public List process() {
+ return locations.stream()
+ .filter(cell -> validCells.contains(cell.getH3Index()))
+ .toList();
+ }
+
+ @Override
+ public List getValidCells() {
+ return validCells.stream()
+ .map(LocationCell::new)
+ .toList();
+ }
+
+ @Override
+ public JPanel getParameterPanel() {
+ JPanel panel = new JPanel(new BorderLayout());
+
+ // Create a map panel
+ MapPanel mapPanel = new MapPanel();
+ panel.add(mapPanel, BorderLayout.CENTER);
+
+ JLabel helpLabel = new JLabel("Use right mouse button to move, mouse wheel to zoom.", SwingConstants.CENTER);
+ panel.add(helpLabel, BorderLayout.SOUTH);
+
+ // Attach the mapPanel as a client property for later retrieval
+ panel.putClientProperty("mapPanel", mapPanel);
+
+ return panel;
+ }
+
+ private List getEllipseBoundary() {
+ System.out.println(getRequirements());
+ List boundary = new ArrayList<>();
+ int numPoints = 100;
+ double radiansRotation = Math.toRadians(rotation);
+
+ for (int i = 0; i < numPoints; i++) {
+ double angle = 2 * Math.PI * i / numPoints;
+ double x = majorAxis * Math.cos(angle);
+ double y = minorAxis * Math.sin(angle);
+
+ // Rotate point
+ double rotatedX = x * Math.cos(radiansRotation) - y * Math.sin(radiansRotation);
+ double rotatedY = x * Math.sin(radiansRotation) + y * Math.cos(radiansRotation);
+
+ // Translate to center
+ boundary.add(new LatLng(center.lat + rotatedY, center.lng + rotatedX));
+ }
+
+ return boundary;
+ }
+
+ private class MapPanel extends JPanel {
+ private final JMapViewer mapViewer;
+ private MapMarkerDot centerMarker;
+ private MapMarkerDot majorAxisMarker;
+ private MapMarkerDot minorAxisMarker;
+ private MapMarkerDot rotationMarker;
+ private MapPolygon ellipsePolygon;
+ private MapMarkerDot selectedMarker;
+
+ private boolean loaded;
+
+ private class JMapViewerFit extends JMapViewer {
+
+ @Override
+ public void tileLoadingFinished(Tile tile, boolean success) {
+ super.tileLoadingFinished(tile, success);
+ if (!loaded & success) {
+ loaded = true;
+ setDisplayToFitMapElements(true, false, true);
+ }
+ }
+ }
+
+ public MapPanel() {
+ setLayout(new BorderLayout());
+ mapViewer = new JMapViewerFit();
+ setPreferredSize(new Dimension(400, 300));
+ mapViewer.setDisplayPosition(new Coordinate(39.8283, -98.5795), 3);
+
+ centerMarker = null; // Center marker not initialized until user sets it
+ majorAxisMarker = null;
+ minorAxisMarker = null;
+ rotationMarker = null;
+ ellipsePolygon = null;
+ selectedMarker = null;
+
+ if (center != null) { // Has existing data, but the UI is being re-initialized. Most common for nested operators.
+ initializeWithExistingEllipse();
+ }
+
+ mapViewer.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ if (centerMarker == null) {
+ Point clickPoint = e.getPoint();
+ ICoordinate coord = mapViewer.getPosition(clickPoint);
+
+ center = new LatLng(coord.getLat(), coord.getLon());
+ centerMarker = new MapMarkerDot(coord.getLat(), coord.getLon());
+ centerMarker.getStyle().setBackColor(Color.GREEN); // Differentiating center marker
+ mapViewer.addMapMarker(centerMarker);
+
+ // Get current viewport
+ double latitudeExtent = mapViewer.getPosition(new Point(0, mapViewer.getHeight())).getLat() -
+ mapViewer.getPosition(new Point(0, 0)).getLat();
+ double longitudeExtent = mapViewer.getPosition(new Point(mapViewer.getWidth(), 0)).getLon() -
+ mapViewer.getPosition(new Point(0, 0)).getLon();
+
+ // Initialize axis markers relative to center
+ majorAxisMarker = new MapMarkerDot(coord.getLat(), coord.getLon() + longitudeExtent / 4);
+ minorAxisMarker = new MapMarkerDot(coord.getLat() - latitudeExtent / 4, coord.getLon());
+ rotationMarker = new MapMarkerDot(coord.getLat(), coord.getLon() + longitudeExtent / 8);
+ rotationMarker.getStyle().setBackColor(Color.RED); // Differentiating rotation marker
+
+ mapViewer.addMapMarker(majorAxisMarker);
+ mapViewer.addMapMarker(minorAxisMarker);
+ mapViewer.addMapMarker(rotationMarker);
+
+ majorAxis = calculateDistance(centerMarker, majorAxisMarker);
+ minorAxis = calculateDistance(centerMarker, minorAxisMarker);
+
+ updateEllipse();
+ mapViewer.repaint();
+ }
+ }
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ if (centerMarker == null) {
+ return; // Do nothing if center is not set
+ }
+
+ Point clickPoint = e.getPoint();
+ MapMarkerDot closestMarker = getClosestMarker(clickPoint);
+ if (calculateDistance(clickPoint, closestMarker) > 8) return; // Too far, assume misclick
+ selectedMarker = closestMarker;
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ selectedMarker = null; // Release the selected marker
+ updateEllipse();
+ }
+ });
+
+ mapViewer.addMouseMotionListener(new MouseMotionAdapter() {
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ if (centerMarker == null || selectedMarker == null) {
+ return; // Do nothing if center or a selected marker is not set
+ }
+
+ Point dragPoint = e.getPoint();
+ ICoordinate coord = mapViewer.getPosition(dragPoint);
+
+ if (selectedMarker == centerMarker) {
+ center = new LatLng(coord.getLat(), coord.getLon());
+ centerMarker.setLat(coord.getLat());
+ centerMarker.setLon(coord.getLon());
+
+ // Constrain axis markers to remain inline with the center
+ updateAxisMarkers();
+ } else if (selectedMarker == majorAxisMarker) {
+ adjustMarkerAlongAxis(coord, true);
+ majorAxis = calculateDistance(centerMarker, majorAxisMarker);
+ } else if (selectedMarker == minorAxisMarker) {
+ adjustMarkerAlongAxis(coord, false);
+ minorAxis = calculateDistance(centerMarker, minorAxisMarker);
+ } else if (selectedMarker == rotationMarker) {
+ rotationMarker.setLat(coord.getLat());
+ rotationMarker.setLon(coord.getLon());
+ rotation = calculateRotation(centerMarker, rotationMarker);
+ updateAxisMarkers(); // Rotate axis markers around the center
+ }
+
+ mapViewer.repaint();
+ }
+ });
+
+ /*
+ mapViewer.addTileLoaderListener(new TileLoaderListener() {
+ @Override
+ public void tileLoadingFinished(Tile tile, boolean success) {
+
+
+ }
+ });*/
+
+ add(mapViewer, BorderLayout.CENTER);
+ }
+
+ private void initializeWithExistingEllipse() {
+ this.centerMarker = new MapMarkerDot(center.lat, center.lng);
+
+ // Initialize axis markers
+ double radiansRotation = Math.toRadians(rotation);
+ this.majorAxisMarker = new MapMarkerDot(
+ center.lat + majorAxis * Math.sin(radiansRotation),
+ center.lng + majorAxis * Math.cos(radiansRotation));
+ this.minorAxisMarker = new MapMarkerDot(
+ center.lat + minorAxis * Math.cos(radiansRotation),
+ center.lng - minorAxis * Math.sin(radiansRotation));
+ this.rotationMarker = new MapMarkerDot(
+ center.lat + majorAxis / 2 * Math.sin(radiansRotation),
+ center.lng + majorAxis / 2 * Math.cos(radiansRotation));
+ this.rotationMarker.getStyle().setBackColor(Color.RED);
+
+ // Add markers to the map
+ mapViewer.addMapMarker(centerMarker);
+ mapViewer.addMapMarker(majorAxisMarker);
+ mapViewer.addMapMarker(minorAxisMarker);
+ mapViewer.addMapMarker(rotationMarker);
+
+ updateEllipse();
+ }
+
+ private MapMarkerDot getClosestMarker(Point clickPoint) {
+ double minDistance = Double.MAX_VALUE;
+ MapMarkerDot closestMarker = null;
+
+ for (MapMarkerDot marker : List.of(centerMarker, majorAxisMarker, minorAxisMarker, rotationMarker)) {
+ if (marker == null) continue;
+
+ Point markerPoint = mapViewer.getMapPosition(new Coordinate(marker.getLat(), marker.getLon()));
+ if (markerPoint != null) {
+ double distance = clickPoint.distance(markerPoint);
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestMarker = marker;
+ }
+ }
+ }
+
+ return closestMarker;
+ }
+
+ private double calculateDistance(MapMarkerDot marker1, MapMarkerDot marker2) {
+ double dx = marker1.getLat() - marker2.getLat();
+ double dy = marker1.getLon() - marker2.getLon();
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ private double calculateDistance(Point clickPoint, MapMarker marker) {
+ Point markerPoint = mapViewer.getMapPosition(new Coordinate(marker.getLat(), marker.getLon()));
+ if (markerPoint == null) return Integer.MAX_VALUE;
+
+ return Math.sqrt((clickPoint.x - markerPoint.x) * (clickPoint.x - markerPoint.x) +
+ (clickPoint.y - markerPoint.y) * (clickPoint.y - markerPoint.y));
+ }
+
+ private double calculateRotation(MapMarkerDot center, MapMarkerDot rotationMarker) {
+ double dx = rotationMarker.getLon() - center.getLon();
+ double dy = rotationMarker.getLat() - center.getLat();
+ return Math.toDegrees(Math.atan2(dy, dx));
+ }
+
+ private void updateAxisMarkers() {
+ if (centerMarker == null || majorAxisMarker == null || minorAxisMarker == null) return;
+
+ double radians = Math.toRadians(rotation);
+
+ // Calculate rotated major axis position
+ double majorDx = majorAxis * Math.cos(radians);
+ double majorDy = majorAxis * Math.sin(radians);
+ majorAxisMarker.setLat(centerMarker.getLat() + majorDy);
+ majorAxisMarker.setLon(centerMarker.getLon() + majorDx);
+
+ // Calculate rotated minor axis position
+ double minorDx = minorAxis * Math.cos(radians + Math.PI / 2);
+ double minorDy = minorAxis * Math.sin(radians + Math.PI / 2);
+ minorAxisMarker.setLat(centerMarker.getLat() + minorDy);
+ minorAxisMarker.setLon(centerMarker.getLon() + minorDx);
+ }
+
+ /**
+ * Adjusts the given marker along the rotated major or minor axis.
+ *
+ * @param coord The new coordinate for the marker.
+ * @param isMajorAxis True if the marker is for the major axis; false for the minor axis.
+ */
+ private void adjustMarkerAlongAxis(ICoordinate coord, boolean isMajorAxis) {
+ if (centerMarker == null) {
+ return; // Do nothing if the center marker is not initialized
+ }
+
+ double dx = coord.getLon() - centerMarker.getLon();
+ double dy = coord.getLat() - centerMarker.getLat();
+
+ // Determine the angle of the axis
+ double axisAngle = Math.toRadians(rotation);
+ if (!isMajorAxis) {
+ axisAngle += Math.PI / 2; // Minor axis is perpendicular to the major axis
+ }
+
+ // Project the point onto the axis
+ double projectionLength = dx * Math.cos(axisAngle) + dy * Math.sin(axisAngle);
+ double projectedX = projectionLength * Math.cos(axisAngle);
+ double projectedY = projectionLength * Math.sin(axisAngle);
+
+ // Update the marker position
+ if (isMajorAxis) {
+ majorAxisMarker.setLon(centerMarker.getLon() + projectedX);
+ majorAxisMarker.setLat(centerMarker.getLat() + projectedY);
+ } else {
+ minorAxisMarker.setLon(centerMarker.getLon() + projectedX);
+ minorAxisMarker.setLat(centerMarker.getLat() + projectedY);
+ }
+ }
+
+
+ public LatLng getCenter() {
+ if (centerMarker == null) {
+ throw new IllegalStateException("Center marker is not set");
+ }
+ return new LatLng(centerMarker.getLat(), centerMarker.getLon());
+ }
+
+ public double getMajorAxis() {
+ return majorAxis;
+ }
+
+ public double getMinorAxis() {
+ return minorAxis;
+ }
+
+ public double getRotation() {
+ return rotation;
+ }
+
+ private void updateEllipse() {
+ if (ellipsePolygon != null) {
+ mapViewer.removeMapPolygon(ellipsePolygon);
+ }
+
+ if (centerMarker != null && majorAxis > 0 && minorAxis > 0) {
+ List coordinates = new ArrayList<>();
+
+ List boundaryPoints = getEllipseBoundary();
+ for (LatLng point : boundaryPoints) {
+ coordinates.add(new Coordinate(point.lat, point.lng));
+ }
+
+ ellipsePolygon = new MapPolygonImpl(coordinates);
+ mapViewer.addMapPolygon(ellipsePolygon);
+ mapViewer.repaint();
+ }
+ }
+ }
+
+ public class EllipseRequirements {
+ public final LatLng center;
+ public final double majorAxis;
+ public final double minorAxis;
+ public final double rotation;
+
+ public EllipseRequirements(LatLng center, double majorAxis, double minorAxis, double rotation) {
+ this.center = center;
+ this.majorAxis = majorAxis;
+ this.minorAxis = minorAxis;
+ this.rotation = rotation;
+ }
+ }
+
+}
+
diff --git a/src/plp/filters/BoundingPolygonFilter.java b/src/plp/filters/BoundingPolygonFilter.java
new file mode 100644
index 0000000..6ec2921
--- /dev/null
+++ b/src/plp/filters/BoundingPolygonFilter.java
@@ -0,0 +1,305 @@
+package plp.filters;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Point;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+import java.io.IOException;
+import java.util.List;
+import java.util.ArrayList;
+
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+
+import org.openstreetmap.gui.jmapviewer.Coordinate;
+import org.openstreetmap.gui.jmapviewer.JMapViewer;
+import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
+import org.openstreetmap.gui.jmapviewer.MapPolygonImpl;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
+import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
+
+import com.uber.h3core.H3Core;
+import com.uber.h3core.util.LatLng;
+
+import plp.Config;
+import plp.filter.InitialFilter;
+import plp.location.LocationCell;
+
+public class BoundingPolygonFilter implements InitialFilter {
+
+ private List boundaryPoints;
+ private List validCells;
+ private List locations;
+ private H3Core h3;
+
+ public BoundingPolygonFilter() {
+ try {
+ h3 = H3Core.newInstance();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to initialize H3 library", e);
+ }
+ boundaryPoints = new ArrayList<>();
+ }
+
+ @Override
+ public void setRequirements(JPanel modifiedParameterPanel) {
+ MapPanel mapPanel = (MapPanel) modifiedParameterPanel.getClientProperty("mapPanel");
+ if (mapPanel == null) {
+ throw new IllegalArgumentException("Parameter panel does not contain the required map panel.");
+ }
+
+ List points = mapPanel.getBoundaryPoints();
+ if (points.size() < 3) {
+ throw new IllegalArgumentException("At least 3 points are required to define a bounding polygon.");
+ }
+
+ boundaryPoints.clear();
+ boundaryPoints = points;
+ validCells = h3.polygonToCells(boundaryPoints, null, Config.H3_RESOLUTION);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void setRequirements(Object requirements) throws IllegalArgumentException {
+ if (requirements instanceof List>) {
+ List> points = (List>) requirements;
+ if (points.size() < 3) {
+ throw new IllegalArgumentException("At least 3 points are required to define a bounding polygon.");
+ }
+ if (!(points.getFirst() instanceof LatLng)) {
+ throw new IllegalArgumentException("Must be an array of LatLng.");
+ }
+ boundaryPoints = (List) points;
+ validCells = h3.polygonToCells(boundaryPoints, null, Config.H3_RESOLUTION);
+ } else {
+ throw new IllegalArgumentException("Invalid requirement type for BoundingBoxFilter");
+ }
+ }
+
+ @Override
+ public String getRequirements() {
+ return boundaryPoints.toString();
+ }
+
+ @Override
+ public void setLocations(List locations) {
+ this.locations = locations;
+ }
+
+ @Override
+ public List process() {
+ return locations.stream()
+ .filter(cell -> validCells.contains(cell.getH3Index()))
+ .toList();
+ }
+
+ public List getValidCells() {
+ return validCells.stream()
+ .map(LocationCell::new)
+ .toList();
+ }
+
+ @Override
+ public JPanel getParameterPanel() {
+ JPanel panel = new JPanel(new BorderLayout());
+
+ // Create a map panel
+ MapPanel mapPanel = new MapPanel();
+ panel.add(mapPanel, BorderLayout.NORTH);
+
+ // Add control buttons
+ JPanel controlPanel = new JPanel(new FlowLayout());
+ JButton addPointButton = new JButton("Add Point");
+ JButton removePointButton = new JButton("Remove Point");
+ JButton clearPointsButton = new JButton("Clear Points");
+
+ controlPanel.add(addPointButton);
+ controlPanel.add(removePointButton);
+ controlPanel.add(clearPointsButton);
+
+ panel.add(controlPanel, BorderLayout.CENTER);
+
+ JLabel helpLabel = new JLabel("Use right mouse button to move, mouse wheel to zoom.", SwingConstants.CENTER);
+ panel.add(helpLabel, BorderLayout.SOUTH);
+
+ // Attach the mapPanel as a client property for later retrieval
+ panel.putClientProperty("mapPanel", mapPanel);
+
+ // Event listeners
+ addPointButton.addActionListener(e -> mapPanel.addPoint());
+ removePointButton.addActionListener(e -> mapPanel.removeLastPoint());
+ clearPointsButton.addActionListener(e -> mapPanel.clearPoints());
+
+ return panel;
+ }
+
+ private class MapPanel extends JPanel {
+ private final JMapViewer mapViewer;
+ private final List markers;
+ private MapPolygon boundingPolygon;
+ private MapMarker draggedMarker;
+
+ private boolean loaded;
+
+ private class JMapViewerFit extends JMapViewer {
+
+ @Override
+ public void tileLoadingFinished(Tile tile, boolean success) {
+ super.tileLoadingFinished(tile, success);
+ if (!loaded & success) {
+ loaded = true;
+ setDisplayToFitMapElements(true, false, true);
+ }
+ }
+ }
+
+ public MapPanel() {
+ setLayout(new BorderLayout());
+ mapViewer = new JMapViewerFit();
+ mapViewer.setDisplayPosition(new Coordinate(39.8283, -98.5795), 3);
+ setPreferredSize(new Dimension(400, 300));
+ markers = new ArrayList<>();
+ importMarkersFromBoundaryPoints();
+
+ mapViewer.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ Point clickPoint = e.getPoint();
+ draggedMarker = markers.stream()
+ .min((marker1, marker2) -> {
+ double dist1 = calculateDistance(clickPoint, marker1);
+ double dist2 = calculateDistance(clickPoint, marker2);
+ return Double.compare(dist1, dist2);
+ })
+ .filter(marker -> calculateDistance(clickPoint, marker) <= 8) // 8 pixels radius
+ .orElse(null);
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ draggedMarker = null; // Stop dragging
+ updateBoundingPolygon();
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ if (draggedMarker == null) {
+ Point clickPoint = e.getPoint();
+ ICoordinate coord = mapViewer.getPosition(clickPoint);
+ MapMarkerDot marker = new MapMarkerDot(coord.getLat(), coord.getLon());
+ markers.add(marker);
+ mapViewer.addMapMarker(marker);
+ updateBoundingPolygon();
+ }
+ }
+ });
+
+ mapViewer.addMouseMotionListener(new MouseMotionAdapter() {
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ if (!isEnabled()) {
+ return;
+ }
+ if (draggedMarker != null) {
+ Point dragPoint = e.getPoint();
+ ICoordinate coord = mapViewer.getPosition(dragPoint);
+ draggedMarker.setLat(coord.getLat());
+ draggedMarker.setLon(coord.getLon());
+ mapViewer.repaint();
+ }
+ }
+ });
+
+ add(mapViewer, BorderLayout.CENTER);
+ }
+
+ private double calculateDistance(Point clickPoint, MapMarker marker) {
+ Point markerPoint = mapViewer.getMapPosition(new Coordinate(marker.getLat(), marker.getLon()));
+ if (markerPoint == null) return Integer.MAX_VALUE;
+
+ return Math.sqrt((clickPoint.x - markerPoint.x) * (clickPoint.x - markerPoint.x) +
+ (clickPoint.y - markerPoint.y) * (clickPoint.y - markerPoint.y));
+ }
+
+ private void updateBoundingPolygon() {
+ if (boundingPolygon != null) {
+ mapViewer.removeMapPolygon(boundingPolygon);
+ }
+
+ if (markers.size() >= 3) {
+ List coordinates = new ArrayList<>();
+ for (MapMarker marker : markers) {
+ coordinates.add(new Coordinate(marker.getLat(), marker.getLon()));
+ }
+ // Close the polygon by adding the first point to the end
+ coordinates.add(coordinates.get(0));
+
+ boundingPolygon = new MapPolygonImpl(coordinates);
+ mapViewer.addMapPolygon(boundingPolygon);
+ mapViewer.repaint();
+ }
+ }
+
+ public void addPoint() {
+ ICoordinate center = mapViewer.getPosition(mapViewer.getWidth() / 2, mapViewer.getHeight() / 2);
+ MapMarkerDot marker;
+ if (center != null) {
+ marker = new MapMarkerDot(center.getLat(), center.getLon());
+ } else {
+ marker = new MapMarkerDot(37.7749, -122.4194); // San Francisco
+ }
+ markers.add(marker);
+ mapViewer.addMapMarker(marker);
+ updateBoundingPolygon();
+ }
+
+ public void removeLastPoint() {
+ if (!markers.isEmpty()) {
+ MapMarker lastMarker = markers.remove(markers.size() - 1);
+ mapViewer.removeMapMarker(lastMarker);
+ updateBoundingPolygon();
+ }
+ }
+
+ public void clearPoints() {
+ for (MapMarker marker : markers) {
+ mapViewer.removeMapMarker(marker);
+ }
+ markers.clear();
+ updateBoundingPolygon();
+ }
+
+ public List getBoundaryPoints() {
+ List boundaryPoints = new ArrayList<>();
+ for (MapMarker marker : markers) {
+ boundaryPoints.add(new LatLng(marker.getLat(), marker.getLon()));
+ }
+ return boundaryPoints;
+ }
+
+ private void importMarkersFromBoundaryPoints() {
+ for (LatLng point : boundaryPoints) {
+ markers.add(new MapMarkerDot(point.lat, point.lng));
+ mapViewer.addMapMarker(markers.getLast());
+ }
+ updateBoundingPolygon();
+ }
+ }
+
+}
diff --git a/src/plp/filters/LightPollutionFilter.java b/src/plp/filters/LightPollutionFilter.java
new file mode 100644
index 0000000..8efa318
--- /dev/null
+++ b/src/plp/filters/LightPollutionFilter.java
@@ -0,0 +1,247 @@
+package plp.filters;
+
+import java.awt.GridLayout;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.io.FileInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import com.uber.h3core.util.LatLng;
+
+import plp.filter.Filter;
+import plp.location.LocationCell;
+import plp.location.LocationUtils;
+
+public class LightPollutionFilter implements Filter {
+ private double minSQM;
+ private List locations;
+ private static final String TILE_PATH = "data/lightpollution/binary_tiles/2022/";
+ private static final String TILE_URL_BASE = "https://github.com/djlorenz/djlorenz.github.io/raw/refs/heads/master/astronomy/binary_tiles/2022/";
+ private static final Map tileDataCache = new HashMap<>(); // Cache for decompressed tiles
+ private static final int TILE_SIZE = 600;
+
+ public LightPollutionFilter() {
+ LocationUtils.initialize();
+ ensureLightPollutionTilesExist();
+ preloadTiles();
+ }
+
+ /**
+ * Ensures the light pollution tiles are downloaded and available in TILE_PATH.
+ */
+ private void ensureLightPollutionTilesExist() {
+ File tileDirectory = new File(TILE_PATH);
+ if (!tileDirectory.exists() || tileDirectory.list().length == 0) {
+ System.out.println("Light pollution tiles not found. Downloading...");
+ tileDirectory.mkdirs();
+ downloadLightPollutionTiles();
+ }
+ }
+
+ /**
+ * Downloads light pollution tiles from the specified GitHub URL and saves them locally.
+ */
+ private void downloadLightPollutionTiles() {
+ try {
+ for (int x = 1; x <= 72; x++) { // 72 tiles in longitude (5 degrees each)
+ for (int y = 1; y <= 28; y++) { // 28 tiles in latitude
+ String fileName = "binary_tile_" + x + "_" + y + ".dat.gz";
+ URL fileUrl = new URI(TILE_URL_BASE + fileName).toURL();
+ Path localFilePath = Path.of(TILE_PATH, fileName);
+
+ System.out.println("Downloading: " + fileUrl);
+ try (InputStream in = fileUrl.openStream()) {
+ Files.copy(in, localFilePath, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ }
+ System.out.println("Light pollution data downloaded successfully.");
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to download light pollution tiles: " + e.getMessage(), e);
+ } catch (URISyntaxException e) {
+ throw new RuntimeException("Failed to download light pollution tiles: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Preloads all binary tiles into memory to optimize access.
+ */
+ private static void preloadTiles() {
+ if (!tileDataCache.isEmpty()) return;
+ File dir = new File(TILE_PATH);
+ File[] files = dir.listFiles((d, name) -> name.endsWith(".dat.gz"));
+ if (files == null) {
+ throw new RuntimeException("Light pollution data folder is empty or invalid: " + TILE_PATH);
+ }
+
+ for (File file : files) {
+ try {
+ String tileKey = file.getName().replace(".dat.gz", "").replace("binary_tile_", "");
+ tileDataCache.put(tileKey, decompressTile(file));
+ } catch (IOException e) {
+ System.err.println("Failed to load tile: " + file.getName());
+ }
+ }
+ }
+
+ /**
+ * Decompresses a GZIP file and returns its byte data.
+ */
+ private static byte[] decompressTile(File file) throws IOException {
+ try (InputStream fileStream = new FileInputStream(file);
+ GZIPInputStream gzipStream = new GZIPInputStream(fileStream);
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
+
+ byte[] temp = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = gzipStream.read(temp)) != -1) {
+ buffer.write(temp, 0, bytesRead);
+ }
+ return buffer.toByteArray();
+ }
+ }
+
+ @Override
+ public void setRequirements(JPanel modifiedParameterPanel) throws IllegalArgumentException {
+ JTextField[] fields = (JTextField[]) modifiedParameterPanel.getClientProperty("fields");
+ double minLat = Double.parseDouble(fields[0].getText());
+ setRequirements(minLat);
+ }
+
+ @Override
+ public void setRequirements(Object requirements) throws IllegalArgumentException {
+ if (requirements instanceof Double) {
+ this.minSQM = (Double) requirements;
+ System.out.println(this.minSQM);
+ } else {
+ throw new IllegalArgumentException("Invalid requirement type for LightPollutionFilter");
+ }
+ }
+
+ @Override
+ public void setLocations(List locations) {
+ this.locations = locations;
+ }
+
+ @Override
+ public List process() {
+ // Simulate filtering locations based on light pollution
+ return locations.stream()
+ .filter(cell -> getSQM(LocationUtils.getLatLng(cell)) >= minSQM)
+ .toList();
+ }
+
+ @Override
+ public JPanel getParameterPanel() {
+ JPanel panel = new JPanel(new GridLayout(1, 2));
+ panel.add(new JLabel("Min SQM:"));
+ JTextField minSQMField = new JTextField(minSQM == 0.0 ? "21.92" : String.valueOf(minSQM));
+ panel.add(minSQMField);
+ panel.putClientProperty("fields", new JTextField[]{minSQMField});
+ return panel;
+ }
+
+ @Override
+ public String getRequirements() {
+ return "Minimum SQM: " + String.valueOf(minSQM);
+ }
+
+ private double getSQM(LocationCell cell) {
+ return getSQM(LocationUtils.getLatLng(cell));
+ }
+
+ private double getSQM(LatLng coords) {
+ return getSQM(coords.lat, coords.lng);
+ }
+
+ /**
+ * Computes the mean SQM for a given LocationCell using the Light Pollution Atlas binary tiles.
+ *
+ * @param latitude The latitude of the location.
+ * @param longitude The longitude of the location.
+ * @return The mean SQM value for the location.
+ */
+ private double getSQM(double latitude, double longitude) {
+
+ // Convert latitude and longitude to tile and grid indices
+ double lonFromDateLine = mod(longitude + 180.0, 360.0);
+ double latFromStart = latitude + 65.0;
+
+ int tileX = (int) Math.floor(lonFromDateLine / 5.0) + 1;
+ int tileY = (int) Math.floor(latFromStart / 5.0) + 1;
+
+ if (tileY < 1 || tileY > 28) {
+ throw new IllegalArgumentException("Location out of bounds (65S to 75N latitude).");
+ }
+
+ String tileKey = tileX + "_" + tileY;
+ byte[] data = tileDataCache.get(tileKey);
+ if (data == null) {
+ throw new RuntimeException("Tile not found in cache: " + tileKey);
+ }
+
+ int ix = (int) Math.round(120.0 * (lonFromDateLine - 5.0 * (tileX - 1) + 1.0 / 240.0));
+ int iy = (int) Math.round(120.0 * (latFromStart - 5.0 * (tileY - 1) + 1.0 / 240.0));
+
+ // Ensure indices are within bounds
+ if (ix < 0 || ix > 600 || iy < 0 || iy > 600) {
+ throw new RuntimeException("Grid indices out of bounds: ix=" + ix + ", iy=" + iy);
+ }
+
+ int firstNumber = 128 * data[0] + data[1];
+ int change = 0;
+
+ for (int i = 1; i < iy; i++) {
+ change += data[TILE_SIZE * i + 1];
+ }
+
+ for (int i = 1; i < ix; i++) {
+ change += data[TILE_SIZE * (iy - 1) + 1 + i];
+ }
+
+ int compressed = firstNumber + change;
+
+ // Ensure compressed value is valid
+ if (compressed < 0) {
+ throw new RuntimeException("Invalid compressed value: " + compressed);
+ }
+
+ double brightnessRatio = compressed2full(compressed);
+ return 22.0 - 5.0 * Math.log10(1.0 + brightnessRatio) / Math.log10(100.0);
+ }
+
+ /**
+ * Handles the modulo operation for positive and negative numbers.
+ * @param x
+ * @param y
+ * @return x mod y
+ */
+ private static double mod(double x, double y) {
+ return ((x % y) + y) % y;
+ }
+
+ /**
+ * Converts the compressed value to brightness ratio.
+ *
+ * @param compressed The compressed brightness value.
+ * @return The full brightness ratio.
+ */
+ private static double compressed2full(int compressed) {
+ return (5.0/195.0) * ( Math.exp(0.0195*compressed) - 1.0);
+ }
+}
diff --git a/src/plp/filters/OperatorFilter.java b/src/plp/filters/OperatorFilter.java
new file mode 100644
index 0000000..a190bbd
--- /dev/null
+++ b/src/plp/filters/OperatorFilter.java
@@ -0,0 +1,96 @@
+package plp.filters;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BoxLayout;
+import javax.swing.JPanel;
+
+import plp.filter.Filter;
+import plp.location.LocationCell;
+import plp.operator.LogicalOperator;
+import plp.operator.OperatorFactory;
+
+public class OperatorFilter implements Filter {
+ private LogicalOperator operator;
+ private final List subFilters = new ArrayList<>();
+ private List locations;
+
+ public OperatorFilter() {}
+
+ public void addFilter(Filter filter) {
+ subFilters.add(filter);
+ }
+
+ @Override
+ public void setRequirements(Object requirements) {
+ if (requirements instanceof LogicalOperator) {
+ this.operator = (LogicalOperator) requirements;
+ } else {
+ throw new IllegalArgumentException("Invalid requirement type for LightPollutionFilter");
+ }
+ }
+
+ @Override
+ public void setLocations(List locations) {
+ for (Filter filter : subFilters) {
+ filter.setLocations(locations);
+ }
+ this.locations = locations;
+ }
+
+ @Override
+ public List process() {
+ if (subFilters.isEmpty()) return List.of();
+
+ List result;
+ switch (operator) {
+ case OR -> result = new ArrayList();
+ case NOT -> result = locations;
+ case XOR -> result = new ArrayList();
+ default -> throw new IllegalArgumentException("Unsupported operator: " + operator);
+ }
+
+ for (int i = 0; i < subFilters.size(); i++) {
+ List nextResult = subFilters.get(i).process();
+
+ switch (operator) {
+ case OR -> result = OperatorFactory.applyOr(result, nextResult);
+ case NOT -> result = OperatorFactory.applyNot(locations, nextResult);
+ case XOR -> result = OperatorFactory.applyExclusiveOr(result, nextResult);
+ default -> throw new IllegalArgumentException("Unsupported operator: " + operator);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public JPanel getParameterPanel() {
+ JPanel allParametersPanel = new JPanel();
+ allParametersPanel.setLayout(new BoxLayout(allParametersPanel, BoxLayout.X_AXIS));
+ for (Filter filter : subFilters) {
+ JPanel panel = filter.getParameterPanel();
+ Component[] comps = panel.getComponents();
+ for (Component comp : comps) {
+ comp.setEnabled(false);
+ }
+ allParametersPanel.add(panel);
+ }
+ return allParametersPanel;
+ }
+
+ @Override
+ public String getRequirements() {
+ String filterDetails = "";
+ for (Filter filter : subFilters) {
+ filterDetails += filter.getClass().getSimpleName() + ": " + filter.getRequirements() + ",";
+ }
+ filterDetails = filterDetails.substring(0, filterDetails.length() - 1);
+ return operator + "[" + filterDetails + "]";
+ }
+
+ public LogicalOperator getOperator() {
+ return operator;
+ }
+}
diff --git a/src/plp/filters/SunWeatherFilter.java b/src/plp/filters/SunWeatherFilter.java
new file mode 100644
index 0000000..b57d19b
--- /dev/null
+++ b/src/plp/filters/SunWeatherFilter.java
@@ -0,0 +1,408 @@
+package plp.filters;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridLayout;
+import java.awt.image.BufferedImage;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.plaf.basic.BasicSliderUI;
+
+import com.uber.h3core.util.LatLng;
+
+import plp.filter.Filter;
+import plp.location.LocationCell;
+import plp.location.LocationUtils;
+
+public class SunWeatherFilter implements Filter {
+
+ private List locations;
+ private static final Map sunriseImages = new HashMap<>();
+ private static final Map sunsetImages = new HashMap<>();
+ private static final Map sunriseColorToPercentageCache = new HashMap<>();
+ private static final Map sunsetColorToPercentageCache = new HashMap<>();
+ private static final int barEnd = 197;
+ private static final int barStart = 886;
+ private Map> colorCache = new HashMap<>();
+ private SunType selectedSunType;
+ private int percentage = -1;
+
+ static {
+ try {
+ // Preload all sunrise images
+ sunriseImages.put("PT", ImageIO.read(new URI("https://sunsetwx.com/sunrise/sunrise_pt.png").toURL()));
+ sunriseImages.put("MT", ImageIO.read(new URI("https://sunsetwx.com/sunrise/sunrise_mt.png").toURL()));
+ sunriseImages.put("CT", ImageIO.read(new URI("https://sunsetwx.com/sunrise/sunrise_ct.png").toURL()));
+ sunriseImages.put("ET", ImageIO.read(new URI("https://sunsetwx.com/sunrise/sunrise_et.png").toURL()));
+
+ // Preload all sunset images
+ sunsetImages.put("PT", ImageIO.read(new URI("https://sunsetwx.com/sunset/sunset_pt.png").toURL()));
+ sunsetImages.put("MT", ImageIO.read(new URI("https://sunsetwx.com/sunset/sunset_mt.png").toURL()));
+ sunsetImages.put("CT", ImageIO.read(new URI("https://sunsetwx.com/sunset/sunset_ct.png").toURL()));
+ sunsetImages.put("ET", ImageIO.read(new URI("https://sunsetwx.com/sunset/sunset_et.png").toURL()));
+
+ // Create color to percentage maps
+ BufferedImage referenceImage = sunriseImages.get("ET"); // Assuming all color bars are the same
+ for (int y = barStart; y >= barEnd; y--) {
+ Color barColor = new Color(referenceImage.getRGB(1350, y));
+ int percentage = (int) ((double) (y-barStart) / (barEnd-barStart) * 100);
+ sunriseColorToPercentageCache.put(barColor, percentage);
+ }
+
+ referenceImage = sunsetImages.get("ET"); // Assuming all color bars are the same
+ for (int y = barStart; y >= barEnd; y--) {
+ Color barColor = new Color(referenceImage.getRGB(1350, y));
+ int percentage = (int) ((double) (y-barStart) / (barEnd-barStart) * 100);
+ sunsetColorToPercentageCache.put(barColor, percentage);
+ }
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to preload weather images: " + e.getMessage(), e);
+ }
+ }
+
+ public SunWeatherFilter() {}
+
+ @Override
+ public String getRequirements() {
+ return selectedSunType + ", Min Quality: " + percentage + "%";
+ }
+
+ @Override
+ public void setLocations(List locations) {
+ this.locations = locations;
+ }
+
+ @Override
+ public List process() {
+ colorCache = new HashMap<>();
+ return locations.stream()
+ .filter(cell -> getPercentageFromColor(getColorAt(LocationUtils.getLatLng(cell))) >= percentage)
+ .toList();
+ }
+
+ @Override
+ public void setRequirements(JPanel modifiedParameterPanel) throws IllegalArgumentException {
+ JComboBox comboBox = (JComboBox) modifiedParameterPanel.getClientProperty("sunTypeComboBox");
+ JSlider slider = (JSlider) modifiedParameterPanel.getClientProperty("percentageSlider");
+
+ if (comboBox == null || slider == null) {
+ throw new IllegalArgumentException("Parameters are missing in the panel.");
+ }
+
+ setRequirements(new SunWeatherRequirements((SunType) comboBox.getSelectedItem(), slider.getValue()));
+ }
+
+ @Override
+ public void setRequirements(Object requirements) throws IllegalArgumentException {
+ if (requirements instanceof String stringRequirement) {
+ String[] parts = stringRequirement.split(",");
+ if (parts.length != 2) {
+ throw new IllegalArgumentException("Invalid string format. Expected 'SunType,Percentage'.");
+ }
+
+ try {
+ this.selectedSunType = SunType.valueOf(parts[0].trim().toUpperCase());
+ this.percentage = Integer.parseInt(parts[1].trim());
+ if (this.percentage < 0 || this.percentage > 100) {
+ throw new IllegalArgumentException("Percentage must be between 0 and 100.");
+ }
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid SunType or Percentage: " + e.getMessage());
+ }
+ } else if (requirements instanceof SunWeatherRequirements swr) {
+ this.selectedSunType = swr.getSunType();
+ this.percentage = swr.getPercentage();
+ if (this.percentage < 0 || this.percentage > 100) {
+ throw new IllegalArgumentException("Percentage must be between 0 and 100.");
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported requirements type: " + requirements.getClass().getName());
+ }
+ }
+
+ @Override
+ public JPanel getParameterPanel() {
+ JPanel panel = new JPanel();
+ panel.setLayout(new GridLayout(2, 2));
+
+ JLabel sunTypeLabel = new JLabel("Sun Event:");
+ JComboBox sunTypeComboBox = new JComboBox<>(SunType.values());
+ if (selectedSunType != null) {sunTypeComboBox.setSelectedItem(selectedSunType);}
+ panel.add(sunTypeLabel);
+ panel.add(sunTypeComboBox);
+
+ JLabel percentageLabel = new JLabel("Minimum Quality Percentage:");
+ ColorBarSlider percentageSlider = new ColorBarSlider(0, 100, percentage == -1 ? 50 : percentage);
+ percentageSlider.setMajorTickSpacing(20);
+ percentageSlider.setPaintTicks(true);
+ percentageSlider.setPaintLabels(true);
+ percentageSlider.setSunType(selectedSunType);
+
+ sunTypeComboBox.addActionListener(e -> {
+ selectedSunType = (SunType) sunTypeComboBox.getSelectedItem();
+ percentageSlider.setSunType(selectedSunType);
+ });
+
+ panel.add(percentageLabel);
+ panel.add(percentageSlider);
+
+ panel.putClientProperty("sunTypeComboBox", sunTypeComboBox);
+ panel.putClientProperty("percentageSlider", percentageSlider);
+
+ return panel;
+ }
+
+ /**
+ * Gets the appropriate weather image based on the longitude and selected sun type.
+ *
+ * @param longitude The longitude of the location.
+ * @return The corresponding BufferedImage.
+ */
+ private BufferedImage getWeatherImage(double longitude) {
+ Map imageMap = selectedSunType == SunType.Sunrise ? sunriseImages : sunsetImages;
+ if (longitude <= -113) {
+ return imageMap.get("PT");
+ } else if (longitude <= -98) {
+ return imageMap.get("MT");
+ } else if (longitude <= -83) {
+ return imageMap.get("CT");
+ } else {
+ return imageMap.get("ET");
+ }
+ }
+
+ /**
+ * Gets the color of the weather image at the specified coordinates.
+ *
+ * @param x The x-coordinate of the pixel.
+ * @param y The y-coordinate of the pixel.
+ * @return The Color of the pixel at the given coordinates.
+ */
+ private Color getColorAt(LatLng location) {
+ BufferedImage weatherImage = getWeatherImage(location.lng);
+ int x = (int) (21.2182 * location.lng + 2726.27);
+ int y = (int) (-25.4166 * location.lat + 1504.25);
+ return getColorAt(weatherImage, x, y);
+ }
+
+ /**
+ * Gets the color of the weather image at the specified coordinates.
+ *
+ * @param image The BufferedImage to analyze.
+ * @param x The x-coordinate of the pixel.
+ * @param y The y-coordinate of the pixel.
+ * @return The Color of the pixel at the given coordinates, adjusted for border effects.
+ */
+ private Color getColorAt(BufferedImage image, int x, int y) {
+ if (image == null) {
+ throw new IllegalStateException("Weather image not initialized.");
+ }
+ if (colorCache.containsKey(x) && colorCache.get(x).containsKey(y)) {
+ return colorCache.get(x).get(y);
+ }
+ int rgb = image.getRGB(x, y);
+ Color color = new Color(rgb);
+
+ // Adjust for potential border dithering
+ if (!isOnCurrentColorbar(color)) {
+ color = adjustForBorder(image, x, y);
+ }
+
+ colorCache.putIfAbsent(x, new HashMap());
+ colorCache.get(x).put(y, color);
+ return color;
+ }
+
+ /**
+ * Adjusts the color by averaging nearby pixels to mitigate border effects.
+ *
+ * @param image The image being analyzed.
+ * @param x The x-coordinate of the pixel.
+ * @param y The y-coordinate of the pixel.
+ * @return The adjusted color.
+ */
+ private Color adjustForBorder(BufferedImage image, int x, int y) {
+ int sampleRadius = 1; // Check neighboring pixels within this radius
+ int r = 0, g = 0, b = 0, count = 0;
+
+ for (int dx = -sampleRadius; dx <= sampleRadius; dx++) {
+ for (int dy = -sampleRadius; dy <= sampleRadius; dy++) {
+ int nx = x + dx;
+ int ny = y + dy;
+
+ if (nx >= 0 && nx < image.getWidth() && ny >= 0 && ny < image.getHeight()) {
+ Color neighborColor = new Color(image.getRGB(nx, ny));
+ if (isOnCurrentColorbar(neighborColor)) { // Only use valid colors
+ r += neighborColor.getRed();
+ g += neighborColor.getGreen();
+ b += neighborColor.getBlue();
+ count++;
+ }
+ }
+ }
+ }
+
+ if (count > 0) {
+ return new Color(r / count, g / count, b / count);
+ } else {
+ return new Color(0, 0, 0); // Fallback to black if all neighbors are borders
+ }
+ }
+
+ /**
+ * Matches a given color to a percentage based on a predefined color bar.
+ * The pink end represents 0%, and the red end represents 100%.
+ *
+ * @param inputColor The input color to match.
+ * @return The corresponding percentage (0–100).
+ */
+ private int getPercentageFromColor(Color inputColor) {
+
+ // Check cache for exact match first
+ if (selectedSunType == SunType.Sunrise) {
+ if (sunriseColorToPercentageCache.containsKey(inputColor)) {
+ return sunriseColorToPercentageCache.get(inputColor);
+ }
+ } else {
+ if (sunsetColorToPercentageCache.containsKey(inputColor)) {
+ return sunsetColorToPercentageCache.get(inputColor);
+ }
+ }
+
+ // Fallback
+ BufferedImage referenceImage = selectedSunType == SunType.Sunrise ? sunriseImages.get("ET") : sunsetImages.get("ET");
+
+ int barEnd = 197;
+ int barStart = 886;
+ int closestY = -1;
+ double closestDistance = Double.MAX_VALUE;
+
+ for (int y = barStart; y >= barEnd; y--) { // Traverse the color bar from bottom to top
+ Color barColor = new Color(referenceImage.getRGB(1350, y)); // 1350 is the middle of the colorbar in the image
+ double distance = calculateColorDistance(barColor, inputColor);
+
+ if (distance < closestDistance) {
+ closestDistance = distance;
+ closestY = y;
+ }
+ }
+
+ return (int) ((double) (closestY-barStart) / (barEnd-barStart) * 100);
+ }
+
+ /**
+ * Determine if the current color is one from the colorbar.
+ *
+ * @param inputColor The input color to check.
+ * @return Whether the color has a perfect match.
+ */
+ private boolean isOnCurrentColorbar(Color inputColor) {
+
+ // Check cache for exact match first
+ if (selectedSunType == SunType.Sunrise) {
+ if (sunriseColorToPercentageCache.containsKey(inputColor)) {
+ return true;
+ }
+ } else {
+ if (sunsetColorToPercentageCache.containsKey(inputColor)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Calculates the distance between two colors in RGB space.
+ *
+ * @param c1 The first color.
+ * @param c2 The second color.
+ * @return The distance between the two colors.
+ */
+ private double calculateColorDistance(Color c1, Color c2) {
+ int redDiff = c1.getRed() - c2.getRed();
+ int greenDiff = c1.getGreen() - c2.getGreen();
+ int blueDiff = c1.getBlue() - c2.getBlue();
+ return Math.sqrt(redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff);
+ }
+
+ /**
+ * SunWeatherRequirements - Helper class for structured input.
+ */
+ public static class SunWeatherRequirements {
+ private final SunType sunType;
+ private final int percentage;
+
+ public SunWeatherRequirements(SunType sunType, int percentage) {
+ this.sunType = sunType;
+ this.percentage = percentage;
+ }
+
+ public SunType getSunType() {
+ return sunType;
+ }
+
+ public int getPercentage() {
+ return percentage;
+ }
+ }
+
+ enum SunType {
+ Sunrise, Sunset
+ }
+
+ /**
+ * Custom JSlider to display the color bar as its background.
+ */
+ public static class ColorBarSlider extends JSlider {
+ private SunType sunType;
+ static final int barEnd = 197; // From sunsetwx image
+ static final int barStart = 886; // From sunsetwx image
+
+ public ColorBarSlider(int min, int max, int value) {
+ super(min, max, value);
+ setUI(new ColorBarSliderUI(this));
+ }
+
+ public void setSunType(SunType sunType) {
+ this.sunType = sunType;
+ repaint();
+ }
+
+ private static class ColorBarSliderUI extends BasicSliderUI {
+ public ColorBarSliderUI(JSlider b) {
+ super(b);
+ }
+
+ @Override
+ public void paintTrack(Graphics grphcs) {
+ Graphics2D g2 = (Graphics2D) grphcs.create();
+ BufferedImage referenceImage = ((ColorBarSlider) slider).sunType == SunType.Sunset
+ ? sunsetImages.get("ET") : sunriseImages.get("ET");
+
+ if (referenceImage != null) {
+ int trackWidth = trackRect.width;
+ int trackHeight = trackRect.height;
+ int trackStart = trackRect.x;
+ int trackEnd = trackStart + trackRect.width;
+ for (int x = trackStart; x < trackEnd; x++) {
+ Color barColor = new Color(referenceImage.getRGB(1350, (int)(barStart - (((x-trackStart)*1.0 / trackWidth) * (barStart - barEnd)) )));
+ g2.setColor(barColor);
+ g2.fillRect(x, 0, 1, trackHeight);
+ }
+ }
+ g2.dispose();
+ }
+ }
+ }
+}
diff --git a/src/plp/location/LocationCell.java b/src/plp/location/LocationCell.java
new file mode 100644
index 0000000..d736a66
--- /dev/null
+++ b/src/plp/location/LocationCell.java
@@ -0,0 +1,18 @@
+package plp.location;
+
+public class LocationCell {
+ private final Long h3Index;
+
+ public LocationCell(Long h3Index) {
+ this.h3Index = h3Index;
+ }
+
+ public Long getH3Index() {
+ return h3Index;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(h3Index);
+ }
+}
\ No newline at end of file
diff --git a/src/plp/location/LocationUtils.java b/src/plp/location/LocationUtils.java
new file mode 100644
index 0000000..391ccf9
--- /dev/null
+++ b/src/plp/location/LocationUtils.java
@@ -0,0 +1,32 @@
+package plp.location;
+
+import java.io.IOException;
+
+import com.uber.h3core.H3Core;
+import com.uber.h3core.util.LatLng;
+
+public class LocationUtils {
+ private static H3Core h3;
+
+ /**
+ * Initializes the Util with H3
+ */
+ public static void initialize() {
+ if (h3 != null) return;
+ try {
+ h3 = H3Core.newInstance();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to initialize H3 library", e);
+ }
+ }
+
+ /**
+ * Get the latitude and longitude of the center of a cell
+ * @param LocationCell cell
+ * @return The LatLng H3 object for the position of the cell
+ */
+ public static LatLng getLatLng(LocationCell cell) {
+ if (h3 == null) initialize();
+ return h3.cellToLatLng(cell.getH3Index());
+ }
+}
diff --git a/src/plp/operator/LogicalOperator.java b/src/plp/operator/LogicalOperator.java
new file mode 100644
index 0000000..7eaed21
--- /dev/null
+++ b/src/plp/operator/LogicalOperator.java
@@ -0,0 +1,5 @@
+package plp.operator;
+
+public enum LogicalOperator {
+ OR, XOR, NOT
+}
diff --git a/src/plp/operator/OperatorFactory.java b/src/plp/operator/OperatorFactory.java
new file mode 100644
index 0000000..fdd0656
--- /dev/null
+++ b/src/plp/operator/OperatorFactory.java
@@ -0,0 +1,42 @@
+package plp.operator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import plp.location.LocationCell;
+
+public class OperatorFactory {
+ public static List applyAnd(List list1, List list2) {
+ return list1.stream().filter(list2::contains).toList();
+ }
+
+ public static List applyOr(List list1, List list2) {
+ Set resultSet = new HashSet<>(list1);
+ resultSet.addAll(list2);
+ return new ArrayList<>(resultSet);
+ }
+
+ public static List applyIntersect(List list1, List list2) {
+ return applyAnd(list1, list2);
+ }
+
+ public static List applyNot(List list1, List list2) {
+ return list1.stream()
+ .filter(cell -> !list2.contains(cell))
+ .toList();
+ }
+
+ public static List applyExclusiveOr(List list1, List list2) {
+ Set resultSet = new HashSet<>(list1);
+ resultSet.addAll(list2);
+
+ Set intersection = new HashSet<>(list1);
+ intersection.retainAll(list2);
+
+ resultSet.removeAll(intersection);
+
+ return new ArrayList<>(resultSet);
+ }
+}
diff --git a/src/plp/output/KMLGenerator.java b/src/plp/output/KMLGenerator.java
new file mode 100644
index 0000000..6c7b003
--- /dev/null
+++ b/src/plp/output/KMLGenerator.java
@@ -0,0 +1,275 @@
+package plp.output;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.awt.Desktop;
+import java.io.File;
+
+import com.uber.h3core.H3Core;
+import com.uber.h3core.util.LatLng;
+
+import de.micromata.opengis.kml.v_2_2_0.Document;
+import de.micromata.opengis.kml.v_2_2_0.Kml;
+import de.micromata.opengis.kml.v_2_2_0.LinearRing;
+import de.micromata.opengis.kml.v_2_2_0.Placemark;
+import de.micromata.opengis.kml.v_2_2_0.Polygon;
+import plp.location.LocationCell;
+
+public class KMLGenerator {
+ public static void generateKML(List locations, String fileName) {
+ System.out.println("Generating KML: " + fileName);
+ try {
+ H3Core h3 = H3Core.newInstance();
+ Kml kml = new Kml();
+ Document document = kml.createAndSetDocument().withName("Filtered Hexagons");
+
+ System.out.println("\tAmalgomating...");
+ List> outerBoundaries = amalgamateHexagons(locations, h3);
+
+ // Separate outer rings and holes
+ System.out.println("\tAssociating holes...");
+ Map, List>> polygonsWithHoles = detectAndAssociateHoles(outerBoundaries);
+
+ System.out.println("\tDrawing...");
+ for (Map.Entry, List>> entry : polygonsWithHoles.entrySet()) {
+ List outerRing = entry.getKey();
+ List> holes = entry.getValue();
+
+ Placemark placemark = document.createAndAddPlacemark().withName(String.valueOf(outerRing.getFirst()));
+ placemark.createAndAddStyle().createAndSetPolyStyle().withColor("aa0000ff"); // Red color in KML (ABGR format)
+ Polygon polygon = placemark.createAndSetPolygon();
+
+ // Add outer boundary
+ LinearRing outerBoundary = polygon.createAndSetOuterBoundaryIs().createAndSetLinearRing();
+ for (LatLng coord : outerRing) {
+ outerBoundary.addToCoordinates(coord.lng, coord.lat, 0);
+ }
+ // Close the outer ring
+ LatLng first = outerRing.get(0);
+ outerBoundary.addToCoordinates(first.lng, first.lat, 0);
+
+ // Add holes as inner boundaries
+ for (List hole : holes) {
+ LinearRing innerBoundary = polygon.createAndAddInnerBoundaryIs().createAndSetLinearRing();
+ for (LatLng coord : hole) {
+ innerBoundary.addToCoordinates(coord.lng, coord.lat, 0);
+ }
+ // Close the hole ring
+ LatLng holeFirst = hole.get(0);
+ innerBoundary.addToCoordinates(holeFirst.lng, holeFirst.lat, 0);
+ }
+ }
+
+ kml.marshal(new File(fileName));
+ System.out.println("KML file generated: " + fileName);
+ } catch (IOException e) {
+ System.err.println("Failed to generate KML: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Amalgamates connected hexagon cells by merging shared edges and returning outer boundaries.
+ *
+ * @param hexagonCells List of LocationCells (H3 indexes).
+ * @param h3 H3Core instance.
+ * @return List of outer boundaries represented as lists of GeoCoords.
+ */
+ private static List> amalgamateHexagons(List hexagonCells, H3Core h3) {
+ // Store the unique edges of all hexagons
+ Set edgeSet = new HashSet<>();
+
+ for (LocationCell cell : hexagonCells) {
+ Long h3Index = cell.getH3Index();
+ List boundary = h3.cellToBoundary(h3Index);
+
+ // Loop through vertices to create edges
+ for (int i = 0; i < boundary.size(); i++) {
+ LatLng start = boundary.get(i);
+ LatLng end = boundary.get((i + 1) % boundary.size());
+
+ Edge edge = new Edge(start, end);
+ if (!edgeSet.remove(edge)) {
+ edgeSet.add(edge);
+ }
+ }
+ }
+
+ // Build polygons from remaining unique edges
+ return buildPolygonsFromEdges(edgeSet);
+ }
+
+ /**
+ * Builds polygons from unique edges by connecting them into rings.
+ *
+ * @param edges Set of unique edges.
+ * @return List of polygons represented as lists of GeoCoords.
+ */
+ private static List> buildPolygonsFromEdges(Set edges) {
+ List> polygons = new ArrayList<>();
+
+ while (!edges.isEmpty()) {
+ List polygon = new ArrayList<>();
+ Edge currentEdge = edges.iterator().next();
+ edges.remove(currentEdge);
+
+ polygon.add(currentEdge.start);
+ LatLng nextPoint = currentEdge.end;
+
+ while (!nextPoint.equals(polygon.get(0))) {
+ Iterator iterator = edges.iterator();
+ while (iterator.hasNext()) {
+ Edge edge = iterator.next();
+ if (edge.start.equals(nextPoint)) {
+ polygon.add(edge.start);
+ nextPoint = edge.end;
+ iterator.remove();
+ break;
+ } else if (edge.end.equals(nextPoint)) {
+ polygon.add(edge.end);
+ nextPoint = edge.start;
+ iterator.remove();
+ break;
+ }
+ }
+ }
+
+ polygons.add(polygon);
+ }
+
+ return polygons;
+ }
+
+ /**
+ * Detects and associates holes and islands with their correct parent polygons.
+ *
+ * @param allBoundaries List of all boundaries (both outer polygons and potential holes).
+ * @return A map of outer boundaries to their associated holes and islands.
+ */
+ private static Map, List>> detectAndAssociateHoles(List> allBoundaries) {
+ Map, List>> result = new LinkedHashMap<>();
+
+ // Step 1: Sort boundaries by size (larger boundaries first)
+ allBoundaries.sort((a, b) -> Integer.compare(b.size(), a.size())); // Descending size
+
+ // Step 2: Build a hierarchy of polygons
+ List nodes = new ArrayList<>();
+ for (List boundary : allBoundaries) {
+ PolygonNode node = new PolygonNode(boundary);
+ for (PolygonNode parent : nodes) {
+ if (isPolygonInsidePolygon(node.polygon, parent.polygon)) {
+ parent.children.add(node);
+ node.parent = parent;
+ break;
+ }
+ }
+ if (node.parent == null) { // Top-level outer boundary
+ nodes.add(node);
+ }
+ }
+
+ // Step 3: Convert hierarchy into a map of outer boundaries to holes
+ for (PolygonNode node : nodes) {
+ result.put(node.polygon, collectHoles(node));
+ }
+
+ return result;
+ }
+
+ /**
+ * Collects all holes (children) under a given polygon node.
+ */
+ private static List> collectHoles(PolygonNode node) {
+ List> holes = new ArrayList<>();
+ for (PolygonNode child : node.children) {
+ holes.add(child.polygon); // Child polygons are treated as holes
+ }
+ return holes;
+ }
+
+ /**
+ * Determines if one polygon is inside another using the point-in-polygon test.
+ */
+ private static boolean isPolygonInsidePolygon(List inner, List outer) {
+ return isPointInsidePolygon(outer, inner.getFirst());
+ }
+
+ /**
+ * Checks if a point is inside a polygon using the ray-casting algorithm.
+ */
+ private static boolean isPointInsidePolygon(List polygon, LatLng point) {
+ boolean result = false;
+ int j = polygon.size() - 1;
+
+ for (int i = 0; i < polygon.size(); i++) {
+ if ((polygon.get(i).lat > point.lat) != (polygon.get(j).lat > point.lat) &&
+ (point.lng < (polygon.get(j).lng - polygon.get(i).lng) *
+ (point.lat - polygon.get(i).lat) /
+ (polygon.get(j).lat - polygon.get(i).lat) + polygon.get(i).lng)) {
+ result = !result;
+ }
+ j = i;
+ }
+ return result;
+ }
+
+ /**
+ * Represents an edge between two GeoCoords, ensuring symmetry.
+ */
+ private static class Edge {
+ LatLng start, end;
+
+ Edge(LatLng start, LatLng end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+ Edge other = (Edge) obj;
+ return (start.equals(other.start) && end.equals(other.end)) ||
+ (start.equals(other.end) && end.equals(other.start));
+ }
+
+ @Override
+ public int hashCode() {
+ return start.hashCode() + end.hashCode();
+ }
+ }
+
+ /**
+ * A helper class representing a polygon node in the hierarchy.
+ */
+ private static class PolygonNode {
+ List polygon; // The polygon boundary
+ PolygonNode parent; // Parent polygon
+ List children; // Child polygons (holes or islands)
+
+ PolygonNode(List polygon) {
+ this.polygon = polygon;
+ this.children = new ArrayList<>();
+ }
+ }
+
+ public static void openKMLInGoogleEarth(String fileName) {
+ try {
+ File kmlFile = new File(fileName);
+
+ if (Desktop.isDesktopSupported()) {
+ Desktop.getDesktop().open(kmlFile);
+ System.out.println("Opening KML file in Google Earth Pro...");
+ } else {
+ System.out.println("Desktop not supported. Please open " + fileName + " manually.");
+ }
+ } catch (IOException e) {
+ System.err.println("Error opening KML file: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/plp/ui/FilterUI.java b/src/plp/ui/FilterUI.java
new file mode 100644
index 0000000..c26d246
--- /dev/null
+++ b/src/plp/ui/FilterUI.java
@@ -0,0 +1,400 @@
+package plp.ui;
+
+import org.reflections.Reflections;
+
+import plp.filter.DataFilter;
+import plp.filter.Filter;
+import plp.filter.InitialFilter;
+import plp.filters.OperatorFilter;
+import plp.location.LocationCell;
+import plp.operator.LogicalOperator;
+import plp.output.KMLGenerator;
+
+import javax.swing.*;
+import java.awt.*;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * FilterUI - A dynamic UI for managing and configuring filters.
+ *
+ * This class provides a Swing-based graphical user interface that dynamically loads
+ * filters from the "filters" package, renders appropriate parameter input components,
+ * allows users to configure the filters, and executes the filter pipeline to generate
+ * a KML file.
+ *
+ * Key Features:
+ * - Dynamic filter discovery using reflection.
+ * - Custom parameter input components for each filter.
+ * - Visual list of configured filters.
+ * - Execution of filters and KML generation.
+ */
+public class FilterUI extends JFrame {
+ private JComboBox filterSelectionBox;
+ private JPanel parameterPanel;
+ private DefaultListModel filterListModel;
+ private ArrayList addedFilters;
+ private Map availableFilters;
+
+ public FilterUI() throws Exception {
+ super("PrecisionLocationProcessor");
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setLayout(new BorderLayout());
+ setSize(800, 600);
+
+ // Progress bar setup
+ JProgressBar progressBar = new JProgressBar();
+ progressBar.setStringPainted(true);
+ progressBar.setString("Loading filters...");
+ add(progressBar, BorderLayout.SOUTH);
+
+ SwingWorker worker = new SwingWorker<>() {
+ @Override
+ protected Void doInBackground() throws Exception {
+ loadAvailableFilters(progressBar); // Load all filters dynamically
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ remove(progressBar);
+ initializeUIComponents();
+ revalidate();
+ repaint();
+ }
+ };
+
+ worker.execute();
+
+
+ }
+
+ private void initializeUIComponents() {
+ // UI Components
+ filterSelectionBox = new JComboBox<>(availableFilters.keySet().toArray(new String[0]));
+ parameterPanel = new JPanel();
+ filterListModel = new DefaultListModel<>();
+ JList filterList = new JList<>(filterListModel);
+ addedFilters = new ArrayList<>();
+
+ JButton addButton = new JButton("Add Filter");
+ JButton runButton = new JButton("Run Filters");
+ JButton addCompositeButton = new JButton("Add Composite Filter"); // Button to add composite filters
+ JButton removeButton = new JButton("Remove Filter");
+
+ // Layout: Top Panel - Filter Selection
+ JPanel topPanel = new JPanel();
+ topPanel.add(new JLabel("Select Filter:"));
+ topPanel.add(filterSelectionBox);
+ topPanel.add(addButton);
+ topPanel.add(addCompositeButton);
+ topPanel.add(removeButton);
+
+ // Layout: Center Panel - Parameter input and filter list
+ add(topPanel, BorderLayout.NORTH);
+ add(parameterPanel, BorderLayout.CENTER);
+ add(new JScrollPane(filterList), BorderLayout.EAST);
+ add(runButton, BorderLayout.SOUTH);
+
+ filterSelectionBox.addActionListener(e -> {
+ try {
+ updateParameterPanel();
+ } catch (Exception e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ }); // Load parameter panel dynamically
+
+ addButton.addActionListener(e -> {
+ try {
+ addFilter();
+ } catch (Exception e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ }); // Add filter to the list
+
+ removeButton.addActionListener(e -> {
+ int selectedIndex = filterList.getSelectedIndex();
+ if (selectedIndex != -1) {
+ filterListModel.remove(selectedIndex);
+ addedFilters.remove(selectedIndex);
+ } else {
+ JOptionPane.showMessageDialog(this, "No filter selected to remove.");
+ }
+ });
+
+ addCompositeButton.addActionListener(e -> addCompositeFilter()); // Add composite filters
+ runButton.addActionListener(e -> runFilters()); // Execute pipeline and generate KML
+
+ try {
+ updateParameterPanel();
+ } catch (Exception e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ } // Initialize with the first filter's parameters
+ }
+
+ /**
+ * Dynamically loads all classes in the "filters" package that implement the Filter interface.
+ * Populates the availableFilters map with filter names and instances.
+ */
+ private void loadAvailableFilters(JProgressBar progressBar) {
+ availableFilters = new HashMap<>();
+ Reflections reflections = new Reflections("plp.filters");
+ Set> classes = reflections.getSubTypesOf(Filter.class);
+ classes.removeIf(filterClass -> filterClass.equals(OperatorFilter.class) || Modifier.isAbstract(filterClass.getModifiers()));
+
+ int totalClasses = classes.size();
+ int progress = 0;
+
+ for (Class extends Filter> filterClass : classes) {
+ progressBar.setString("Loading filters... " + (progress+1) + "/" + totalClasses + " – " + filterClass.getSimpleName());
+ try {
+ Filter filter = filterClass.getDeclaredConstructor().newInstance();
+ availableFilters.put(filterClass.getSimpleName(), filter);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ progress++;
+ int progressPercentage = (int) ((progress / (double) totalClasses) * 100);
+ progressBar.setValue(progressPercentage);
+ }
+
+ progressBar.setIndeterminate(false);
+ progressBar.setValue(100);
+ progressBar.setString("Filters loaded.");
+ }
+
+ /**
+ * Updates the parameter input panel to match the selected filter.
+ * This dynamically renders the input fields defined by the selected filter.
+ * @throws SecurityException
+ * @throws NoSuchMethodException
+ * @throws InvocationTargetException
+ * @throws IllegalArgumentException
+ * @throws IllegalAccessException
+ * @throws InstantiationException
+ */
+ private void updateParameterPanel() throws Exception {
+ parameterPanel.removeAll(); // Clear existing components
+ String selectedFilter = (String) filterSelectionBox.getSelectedItem();
+ Filter filter = availableFilters.get(selectedFilter).getClass().getDeclaredConstructor().newInstance();
+ if (filter != null) {
+ parameterPanel.add(filter.getParameterPanel()); // Add the filter's parameter UI
+ }
+ parameterPanel.revalidate();
+ parameterPanel.repaint();
+ }
+
+ /**
+ * Adds a filter instance with user-specified parameters to the pipeline.
+ * Validates user input and displays the filter in the configured filter list.
+ * @throws SecurityException
+ * @throws NoSuchMethodException
+ * @throws InvocationTargetException
+ * @throws IllegalArgumentException
+ * @throws IllegalAccessException
+ * @throws InstantiationException
+ *
+ */
+ private void addFilter() throws Exception {
+
+ String selectedFilter = (String) filterSelectionBox.getSelectedItem();
+ Filter filter = availableFilters.get(selectedFilter).getClass().getDeclaredConstructor().newInstance();
+
+ if (filter != null) {
+ try {
+ JPanel filterParameterPanel = (JPanel) parameterPanel.getComponent(0);
+ filter.setRequirements(filterParameterPanel); // Set the requirements dynamically
+ addedFilters.add(filter); // Add filter instance to the list
+ filterListModel.addElement(selectedFilter + ": " + filter.getRequirements());
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(rootPane, "Invalid input: " + ex.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Adds a composite filter with logical operators and sub-filters.
+ * Allows the user to set parameters for sub-filters dynamically.
+ */
+ private void addCompositeFilter() {
+ createCompositeFilter().thenAccept(compositeFilter -> {
+ if (compositeFilter != null) {
+ addedFilters.add(compositeFilter);
+ filterListModel.addElement(compositeFilter.getRequirements());
+ }
+ }).exceptionally(ex -> {
+ ex.printStackTrace(); // Log errors
+ return null;
+ });
+ }
+
+ /**
+ * Adds a composite filter with logical operators and sub-filters.
+ * Allows the user to set parameters for sub-filters dynamically.
+ */
+ private CompletableFuture createCompositeFilter() {
+
+ CompletableFuture future = new CompletableFuture<>();
+
+ // Create a dialog to select sub-filters and operators
+ JFrame dialog = new JFrame("Create Composite Filter");
+ dialog.setSize(700, 800);
+ dialog.setLayout(new BorderLayout());
+ dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+
+ DefaultListModel selectedFiltersModel = new DefaultListModel<>();
+
+ // Get operators dynamically from LogicalOperator enum
+ JComboBox operatorBox = new JComboBox<>(Arrays.stream(LogicalOperator.values())
+ .map(Enum::name).toArray(String[]::new));
+
+ JPanel parameterContainer = new JPanel();
+ parameterContainer.setLayout(new BoxLayout(parameterContainer, BoxLayout.Y_AXIS));
+
+ JButton addSubFilterButton = new JButton("Add Sub-Filter");
+ addSubFilterButton.addActionListener(e -> {
+ try {
+ String selectedFilter = (String) filterSelectionBox.getSelectedItem();
+ selectedFiltersModel.addElement(selectedFilter);
+ Filter filter = availableFilters.get(selectedFilter).getClass().getDeclaredConstructor().newInstance();
+ if (filter != null) {
+ JPanel subFilterPanel = filter.getParameterPanel();
+ subFilterPanel.setBorder(BorderFactory.createTitledBorder(selectedFilter));
+ parameterContainer.add(subFilterPanel);
+ parameterContainer.revalidate();
+ parameterContainer.repaint();
+ }
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(this, "Exception Adding new sub-filter: " + ex.getMessage());
+ }
+ });
+
+ JButton addNestedCompositeButton = new JButton("Add Nested Composite Filter");
+ addNestedCompositeButton.addActionListener(e -> {
+ createCompositeFilter().thenAccept(nestedComposite -> {
+ if (nestedComposite != null) {
+ selectedFiltersModel.addElement("Nested Composite Filter (" + nestedComposite.getOperator().toString() + ")");
+ JPanel nestedPanel = nestedComposite.getParameterPanel();
+ nestedPanel.setBorder(BorderFactory.createTitledBorder("Nested Composite Filter (" + nestedComposite.getOperator().toString() + ")"));
+ nestedPanel.putClientProperty("filter", nestedComposite);
+ parameterContainer.add(nestedPanel);
+ parameterContainer.revalidate();
+ parameterContainer.repaint();
+ }
+ });
+ });
+
+ JButton createButton = new JButton("Create Composite Filter");
+ createButton.addActionListener(e -> {
+ String operator = (String) operatorBox.getSelectedItem();
+ OperatorFilter compositeFilter = new OperatorFilter();
+ compositeFilter.setRequirements(LogicalOperator.valueOf(operator));
+
+ for (Component component : parameterContainer.getComponents()) {
+ if (component instanceof JPanel panel) {
+ Object filterProperty = panel.getClientProperty("filter");
+
+ if (filterProperty instanceof OperatorFilter nestedComposite) {
+ compositeFilter.addFilter(nestedComposite);
+ } else {
+ try {
+ String filterName = selectedFiltersModel.getElementAt(parameterContainer.getComponentZOrder(panel));
+ Filter subFilter = availableFilters.get(filterName).getClass().getDeclaredConstructor().newInstance();
+
+ if (subFilter != null) {
+ try {
+ subFilter.setRequirements(panel);
+ compositeFilter.addFilter(subFilter);
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(this, "Invalid input for sub-filter " + filterName + ": " + ex.getMessage());
+ future.completeExceptionally(ex); // Handle exceptions
+ return;
+ }
+ }
+ } catch (Exception ex) {
+ JOptionPane.showMessageDialog(this, "Exception creating composite filter: " + ex.getMessage());
+ }
+ }
+ }
+ }
+
+ future.complete(compositeFilter); // Complete the future
+ dialog.dispose();
+ });
+
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(e -> {
+ future.complete(null); // Signal cancellation
+ dialog.dispose();
+ });
+
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.add(addSubFilterButton);
+ buttonPanel.add(addNestedCompositeButton);
+ buttonPanel.add(createButton);
+ buttonPanel.add(cancelButton);
+
+ dialog.add(operatorBox, BorderLayout.NORTH);
+ dialog.add(new JScrollPane(parameterContainer), BorderLayout.CENTER);
+ dialog.add(buttonPanel, BorderLayout.SOUTH);
+
+ dialog.setVisible(true);
+ return future; // Return the constructed composite filter
+ }
+
+
+ /**
+ * Executes the filter pipeline, applies all configured filters, and generates a KML file.
+ * Displays a success message upon completion.
+ */
+ private void runFilters() {
+ DataFilter dataFilter = null;
+ InitialFilter initialBounds = null;
+
+ // Find the first InitialFilter in the list
+ for (Filter filter : addedFilters) {
+ if (filter instanceof InitialFilter) {
+ initialBounds = (InitialFilter) filter;
+ dataFilter = new DataFilter(initialBounds);
+ break;
+ }
+ }
+
+ if (dataFilter == null) {
+ JOptionPane.showMessageDialog(this, "Error: An InitialFilter is required to start the pipeline.",
+ "Missing BoundingBoxFilter", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ // Add all configured filters to the pipeline
+ for (Filter filter : addedFilters) {
+ if (filter.equals(initialBounds)) continue;
+ dataFilter.addFilter(filter);
+ }
+
+ // Run the filters and generate KML
+ List filteredLocations = dataFilter.filterLocations();
+ KMLGenerator.generateKML(filteredLocations, "ui_filtered_hexagons.kml");
+ KMLGenerator.openKMLInGoogleEarth("ui_filtered_hexagons.kml");
+ JOptionPane.showMessageDialog(this, "Filters applied! KML file generated: ui_filtered_hexagons.kml");
+ }
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(() -> {
+ try {
+ new FilterUI().setVisible(true);
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ });
+ }
+}
+