GeoWatch Java Geospatial Analyzer
Build a tested Java desktop app that analyzes and visualizes geographic events.
Introduction
30 Second Summary
A regional display can look convincing even when one location is impossible. Without clear checks, bad coordinates can hide inside an otherwise believable map.
In this project, you will build GeoWatch with Java to analyze a fictional feed of geographic events. GeoWatch turns that feed into tested spatial results on a color-coded desktop plot.
What You'll Build
Picture running GeoWatch as the terminal reports rejected coordinates beside a desktop window that maps every valid event by query result.
By the end of this project, you'll have:
- A resilient event feed where an impossible latitude produces a clear rejection without stopping valid events from reaching the screen.
- A color-coded operations plot that distinguishes radius matches, bounding-box matches, overlaps, unmatched events, and the command center.
- A proof trail on GitHub with six passing JUnit tests, a reproducible Maven build, and milestone commits you can explain.
- Secret Mission: Rank the two nearest events and mark them with gold rings.
Are there any prerequisites?
You'll need access to a graphical Fedora or Red Hat Enterprise Linux desktop, a JDK, Maven, Git, Visual Studio Code, and a GitHub account.
The first step confirms Java 17 or later plus Maven 3.6.3 or later.
Before We Start
Before any hands-on work begins, commit to building an explainable Java geospatial analyzer for fictional regional command-center events. Naming why you want to explain its implementation in an interview keeps every later choice focused.
Verify and Initialize the Maven Project
GeoWatch needs a reproducible terminal build before you write its event model. A known Java toolchain lets the same source compile from your Fedora or RHEL desktop.
This step verifies Maven plus Git. You will also create the standard source layout that every later class builds on.
In this step, get ready to:
- Verify compatible Java, Maven, and Git installations.
- Create the Maven project layout and configuration files.
- Run the first build and commit the project foundation.
Verify the terminal toolchain
JUnit requires Java 17 or later. The pinned Maven plugins require Maven 3.6.3 or later.
The full OpenJDK development packages also provide the compiler and desktop libraries that GeoWatch needs for its later Swing window.
- Check all four terminal tools by running these commands:
java --version
javac --version
mvn --version
git --version
What do these checks prove?
- The java output identifies the runtime that launches GeoWatch.
- The javac output confirms that the Java compiler is available.
- The mvn output identifies Maven plus the Java runtime it uses.
- The git output confirms that version control is ready.
Choose the Java result that matches your terminal output.
✔️ Java and javac are version 17 or higher
Your Java runtime and compiler are compatible. Both parts of the toolchain are ready for GeoWatch.
ⓧ Java or javac is older than 17
Install the full Java 17 development packages for your Linux distribution. These commands request your administrator password because they install system packages.
Package installation can take a few minutes while your system downloads the runtime and compiler.
Fedora
- Install Java 17 with desktop libraries and development tools by running:
sudo dnf install java-17-openjdk java-17-openjdk-devel
The Fedora package manager installs the Java runtime plus the development tools.
- Confirm the installed runtime and compiler by running:
java --version
javac --version
You should now see version 17 or higher in both outputs.
RHEL
- Install Java 17 with desktop libraries and development tools by running:
sudo yum install java-17-openjdk java-17-openjdk-devel
The RHEL package manager installs the Java runtime plus the development tools.
- Confirm the installed runtime and compiler by running:
java --version
javac --version
You should now see version 17 or higher in both outputs.
Still seeing an older Java version?
Your terminal may still resolve an older Java installation first. Use your system's Java selection settings to choose the newly installed Java 17 runtime and compiler.
Ask for help with the versions shown in your terminal: help me select Java 17 on Fedora or RHEL
ⓧ Java or javac is missing
Install the full Java 17 development packages for your Linux distribution. These commands request your administrator password because they install system packages.
Package installation can take a few minutes while your system downloads the runtime and compiler.
Fedora
- Install Java 17 with desktop libraries and development tools by running:
sudo dnf install java-17-openjdk java-17-openjdk-devel
The Fedora package manager installs the missing Java runtime plus the development tools.
- Confirm the installed runtime and compiler by running:
java --version
javac --version
You should now see version 17 or higher in both outputs.
RHEL
- Install Java 17 with desktop libraries and development tools by running:
sudo yum install java-17-openjdk java-17-openjdk-devel
The RHEL package manager installs the missing Java runtime plus the development tools.
- Confirm the installed runtime and compiler by running:
java --version
javac --version
You should now see version 17 or higher in both outputs.
Java command still missing?
Confirm that the package installation completed without a repository or permission failure. Start a fresh terminal session after a successful installation.
Ask for help with the package-manager output: help me find why Java 17 is still missing
Choose the Maven result that matches your terminal output.
✔️ I see Maven 3.6.3 or higher
Your Maven installation supports the compiler and test plugins used by GeoWatch. The build tool is ready.
ⓧ I see an older Maven version
Install the Maven package supplied for your Linux distribution. The installation requests administrator approval because it changes system packages.
Fedora
- Install Maven by running:
sudo dnf install maven
The Fedora package manager installs Maven and its required supporting packages.
- Check the installed Maven version by running:
mvn --version
You should now see Maven 3.6.3 or higher.
RHEL
- Install Maven by running:
sudo yum install maven
The RHEL package manager installs Maven and its required supporting packages.
- Check the installed Maven version by running:
mvn --version
You should now see Maven 3.6.3 or higher.
Still seeing an older Maven version?
Your terminal may be resolving an older Maven installation first. Start a fresh terminal before checking the version again.
Ask for help with the version and installation output: help me diagnose my Maven version
ⓧ Maven command is missing
Install Maven through the package manager for your Linux distribution. The installation requests administrator approval because it changes system packages.
Fedora
- Install Maven by running:
sudo dnf install maven
The Fedora package manager installs the missing Maven command and its supporting packages.
- Check the installed Maven version by running:
mvn --version
You should now see Maven 3.6.3 or higher.
RHEL
- Install Maven by running:
sudo yum install maven
The RHEL package manager installs the missing Maven command and its supporting packages.
- Check the installed Maven version by running:
mvn --version
You should now see Maven 3.6.3 or higher.
Maven command still missing?
Confirm that the package installation completed without a repository or permission failure. Start a fresh terminal session after a successful installation.
Ask for help with the package-manager output: help me find why Maven is still missing
Your Git check should also show a version number. That confirms the final tool in your terminal workflow is available.
Create the Maven project layout
Maven uses a conventional directory layout to separate application source from test source. The pom.xml file records the Java target plus every pinned build dependency.
- Move to your Desktop by running this command:
cd ~/Desktop
Your terminal now uses the Desktop as the location for the new project directory.
- Create the project directory and both Java package directories by running:
mkdir -p geowatch-analyzer/src/main/java/com/nextwork/geo geowatch-analyzer/src/test/java/com/nextwork/geo
What does this command create?
The geowatch-analyzer directory contains the whole project. Maven reads application classes from src/main/java/com/nextwork/geo.
Maven reads test classes from src/test/java/com/nextwork/geo. The package paths keep production code and tests aligned under com.nextwork.geo.
- Enter the new project directory by running:
cd geowatch-analyzer
The terminal is now positioned inside the geowatch-analyzer directory.
- Initialize the local repository on the main branch by running:
git init -b main
What did Git initialize?
Git creates its repository metadata inside the project directory. The -b main option names the initial branch main.
- Open the current project directory in Visual Studio Code by running:
code .
You should see geowatch-analyzer in the VS Code Explorer. Its src directory contains the empty main and test package paths.
Project did not open in VS Code?
- Start Visual Studio Code through your Fedora or RHEL application search.
- Select File from the top menu.
- Select Open Folder.
- Choose the geowatch-analyzer directory on your Desktop.
Ask for help if the command-line launcher is unavailable: help me open this Linux project in VS Code
The Maven Project Object Model gives the build one source of truth. It pins JUnit 6.1.3 plus the compiler and test plugins.
- Create pom.xml in the current VS Code project by running this command in the terminal:
code pom.xml
VS Code opens a new pom.xml editor inside the current project.
- Paste the project identity and dependency section into pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nextwork</groupId>
<artifactId>geowatch-analyzer</artifactId>
<version>1.0-SNAPSHOT</version>
<name>GeoWatch Analyzer</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
What does this section define?
- The project coordinates identify GeoWatch as com.nextwork:geowatch-analyzer:1.0-SNAPSHOT.
- The compiler release targets Java 17.
- The JUnit dependency gives the test source access to the Jupiter API and test engine.
- The test scope keeps JUnit out of the application runtime.
- Append the build-plugin section directly below the closing </dependencies> line:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>
What do these plugins control?
Maven Compiler Plugin 3.16.0 compiles the Java source for release 17. Maven Surefire Plugin 3.6.0 discovers and runs the JUnit tests.
✔️ Awesome, I've got everything!
Your two sections now form one complete Maven project configuration.
ⓧ I'd like to double check the full code
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nextwork</groupId>
<artifactId>geowatch-analyzer</artifactId>
<version>1.0-SNAPSHOT</version>
<name>GeoWatch Analyzer</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>
- Save pom.xml.
- Return to the Fedora or RHEL terminal from earlier.
The first Maven run can take a few minutes while it downloads the pinned plugins. A short pause during this download is normal.
- Test the new Maven configuration by running:
mvn test
What did Maven prove?
Maven reads pom.xml and resolves the pinned build components. It then completes the test lifecycle against the empty source layout.
The terminal should end with BUILD SUCCESS. That result proves the project configuration is valid before application code exists.
That is your first green GeoWatch build. The terminal can now reproduce the project foundation from its configuration.
Maven build did not succeed?
Compare the opening and closing XML tags in pom.xml with the full-file reference. Also confirm that both plugin version values match the reference.
Ask for help with the first Maven error shown in your terminal: help me diagnose this Maven setup failure
Maven writes generated build output into target. A .gitignore file keeps that output plus local editor data out of future commits.
- Create .gitignore in the current VS Code project by running:
code .gitignore
VS Code opens a new .gitignore editor inside geowatch-analyzer.
- Paste these ignore patterns into .gitignore:
target/
.vscode/
*.class
What do these patterns exclude?
- The target/ pattern excludes Maven build output.
- The .vscode/ pattern excludes local editor settings.
- The *.class pattern excludes compiled Java class files.
✔️ Awesome, I've got everything!
Your ignore file now covers generated build output and local editor files.
ⓧ I'd like to double check the full code
target/
.vscode/
*.class
- Save .gitignore.
Commit and verify the foundation
The first commit creates a recovery point for the build configuration. Git should track the two project files while leaving Maven output outside the snapshot.
- Inspect the current repository state by running:
git status
What should Git report?
Git should report the pom.xml and .gitignore files as untracked. The generated target directory should stay out of the report.
The empty source package directories remain on disk. Git begins tracking their contents when you add Java files in the next step.
- Stage the project files and record the foundation by running:
git add .
git commit -m "chore: initialize Maven project"
What did this commit save?
Git stages the project configuration before recording a snapshot named chore: initialize Maven project. That snapshot captures the reproducible starting point for every later feature.
Commit could not identify you?
Git needs an author name plus an email address before it can create a commit. Configure the identity used by your existing Git setup before repeating the commit command.
Ask for help with the message shown in your terminal: help me complete my first Git commit
Before you run the final check, do you expect Maven and Git to agree that the foundation is ready?
- Verify the build and repository state by running:
mvn test
git status
What should you see?
- Maven ends with BUILD SUCCESS.
- Git reports that you are on the main branch.
- Git reports a clean worktree after the foundation commit.
Your Java 17 Maven foundation is built and committed. Next, you will plot GeoWatch's first fictional event feed in a desktop window.
Plot the Flawed Event Feed
Your Maven foundation is committed. Your analyzer now needs a visible desktop pipeline that turns event coordinates into markers.
Java Swing gives you immediate visual proof that the application can load data and paint a window. You will feed it five events before comparing the loaded count with the markers inside the grid.
In this step, get ready to:
- Model five fictional geographic events with encapsulated fields.
- Render event coordinates on a latitude-longitude grid.
- Run the flawed feed to inspect its loaded count and visible markers.
Define the event model
A GeoEvent object keeps each event's label and coordinates together. Private fields provide encapsulation by controlling how the rest of the application reads that data.
- In the Visual Studio Code Explorer sidebar from earlier, select the src/main/java/com/nextwork/geo folder.
- Click the New File icon at the top of the Explorer sidebar.
- Enter GeoEvent.java as the file name.
- Define the event fields and constructor by pasting this code into GeoEvent.java:
package com.nextwork.geo;
public class GeoEvent {
private final String name;
private final String category;
private final int severity;
private final double latitude;
private final double longitude;
public GeoEvent(
String name,
String category,
int severity,
double latitude,
double longitude) {
this.name = name;
this.category = category;
this.severity = severity;
this.latitude = latitude;
this.longitude = longitude;
}
}
What does this code do?
- The five private fields store the identity and location of one fictional event.
- The constructor receives every value required to create a complete event.
- The constructor currently assigns coordinates exactly as supplied.
- Save GeoEvent.java.
- Switch back to the Fedora or RHEL terminal from earlier.
- Compile the constructor by running this command:
mvn compile
What does this check prove?
Maven compiles GeoEvent.java into the project's target/classes directory. A successful finish confirms the package and constructor syntax are valid.
Your first application class now compiles successfully. That gives you a safe checkpoint before you expand its public interface.
Does the constructor fail to compile?
Check that GeoEvent.java sits inside src/main/java/com/nextwork/geo. Confirm that the package declaration is the first line.
Compare the opening and closing braces if Maven points to the end of the file.
Help me fix my GeoEvent constructor compilation error.
The application needs read-only access to each private value when it paints a marker. Getter methods expose those values without allowing another class to replace the fields.
- Return to GeoEvent.java in the editor.
- Place your cursor immediately before the final closing brace.
- Add the five getters by pasting this code:
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public int getSeverity() {
return severity;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
Why use getters?
Each getter returns one private field without changing it. The plot can now read names and coordinates while the event remains immutable.
- Save GeoEvent.java.
- Switch back to the terminal from earlier.
- Compile the completed event model by running:
mvn compile
What does the second compile prove?
This compile checks the constructor and all five getter signatures together. Maven should finish successfully with the completed event model.
Do the getters cause a compilation error?
Confirm every getter is inside the GeoEvent class. A getter placed after the class's final brace cannot compile.
Check that each return statement matches its field name exactly.
Help me find the error in my GeoEvent getters.
✔️ Awesome, I've got everything!
Great. Save GeoEvent.java before you build the visual layer.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
public class GeoEvent {
private final String name;
private final String category;
private final int severity;
private final double latitude;
private final double longitude;
public GeoEvent(
String name,
String category,
int severity,
double latitude,
double longitude) {
this.name = name;
this.category = category;
this.severity = severity;
this.latitude = latitude;
this.longitude = longitude;
}
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public int getSeverity() {
return severity;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
}
Draw the regional grid
A Swing panel is a surface that can paint shapes and text. Your MapPanel will convert longitude into horizontal pixels and latitude into vertical pixels.
- In the Explorer sidebar, select the src/main/java/com/nextwork/geo folder.
- Click the New File icon.
- Enter MapPanel.java as the file name.
- Create the panel shell by pasting this code into MapPanel.java:
package com.nextwork.geo;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.List;
import javax.swing.JPanel;
public class MapPanel extends JPanel {
private static final double MIN_LATITUDE = 39.0;
private static final double MAX_LATITUDE = 40.6;
private static final double MIN_LONGITUDE = -105.8;
private static final double MAX_LONGITUDE = -104.1;
private static final int PADDING = 50;
private final List<GeoEvent> events;
public MapPanel(List<GeoEvent> events) {
this.events = events;
setPreferredSize(new Dimension(900, 550));
setBackground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
}
}
What does the panel shell define?
- The four coordinate constants define the geographic range shown by the grid.
- The padding keeps labels and markers away from the window edges.
- The constructor stores the event list and gives the panel a preferred size.
- The painting override currently clears the panel through its superclass.
- Save MapPanel.java.
- Switch back to the terminal from earlier.
- Compile the panel shell by running:
mvn compile
What does this compile confirm?
The successful compile confirms that Swing can load the panel class and its event collection. It also checks every Java desktop import used so far.
Does MapPanel fail to compile?
Check that the file name uses the exact capitalization MapPanel.java. Confirm the class extends JPanel.
Help me fix my MapPanel shell compilation error.
Coordinate-to-pixel conversion measures where a value sits between the minimum and maximum. Multiplying that fraction by the drawable width or height produces its screen position.
- Return to MapPanel.java.
- Place your cursor immediately before the final class brace.
- Add the coordinate conversion methods by pasting:
private int toX(double longitude) {
double fraction = (longitude - MIN_LONGITUDE)
/ (MAX_LONGITUDE - MIN_LONGITUDE);
return PADDING + (int) Math.round(fraction * (getWidth() - 2 * PADDING));
}
private int toY(double latitude) {
double fraction = (MAX_LATITUDE - latitude)
/ (MAX_LATITUDE - MIN_LATITUDE);
return PADDING + (int) Math.round(fraction * (getHeight() - 2 * PADDING));
}
How do coordinates become pixels?
The toX() method measures longitude from the grid's western edge. The toY() method reverses latitude so larger values appear higher on the panel.
- Save MapPanel.java.
- Switch back to the terminal.
- Compile the conversion methods by running:
mvn compile
What does this check confirm?
Maven now checks both conversion methods against the panel constants and inherited size methods. A successful finish confirms the pixel calculations are valid Java.
Do the conversion methods fail?
Confirm both methods sit inside the MapPanel class. Check the parentheses around each width and height calculation.
Help me debug my coordinate-to-pixel methods.
The grid gives each marker geographic context. Vertical lines represent longitudes while horizontal lines represent latitudes.
- Place your cursor before the final class brace in MapPanel.java.
- Add the grid renderer by pasting this method:
private void drawGrid(Graphics2D graphics2D) {
int plotWidth = getWidth() - 2 * PADDING;
int plotHeight = getHeight() - 2 * PADDING;
graphics2D.setColor(new Color(225, 225, 225));
double[] longitudes = {-105.5, -105.0, -104.5};
for (double longitude : longitudes) {
int x = toX(longitude);
graphics2D.drawLine(x, PADDING, x, PADDING + plotHeight);
graphics2D.drawString(longitude + "°", x - 20, PADDING + plotHeight + 20);
}
double[] latitudes = {39.0, 39.5, 40.0, 40.5};
for (double latitude : latitudes) {
int y = toY(latitude);
graphics2D.drawLine(PADDING, y, PADDING + plotWidth, y);
graphics2D.drawString(latitude + "°", 8, y + 5);
}
graphics2D.setColor(Color.DARK_GRAY);
graphics2D.drawRect(PADDING, PADDING, plotWidth, plotHeight);
graphics2D.drawString("Loaded events: " + events.size(), PADDING, 22);
}
What does the grid renderer do?
- The longitude loop draws three vertical guide lines with coordinate labels.
- The latitude loop draws four horizontal guide lines with coordinate labels.
- The border marks the visible plotting area.
- The loaded count comes directly from the event list size.
- Save MapPanel.java.
- Switch back to the terminal.
- Compile the grid renderer by running:
mvn compile
What does the grid compile prove?
This check confirms that the loops can call both conversion methods and Java 2D drawing operations. The panel is now ready to place events.
Does the grid method fail?
Check that the longitude and latitude arrays use braces around their values. Confirm that drawGrid() appears inside the class.
Help me fix my drawGrid method.
Each event marker uses the same coordinate conversion as the grid. Its label is drawn beside the marker so you can identify the source object.
- Place your cursor before the final class brace in MapPanel.java.
- Add the event renderer by pasting this method:
private void drawEvent(Graphics2D graphics2D, GeoEvent event) {
graphics2D.setColor(new Color(100, 116, 139));
int x = toX(event.getLongitude());
int y = toY(event.getLatitude());
graphics2D.fillOval(x - 6, y - 6, 12, 12);
graphics2D.drawString(event.getName(), x + 8, y - 8);
}
How is each event painted?
The method reads an event's coordinates through its getters. It paints a gray circle at the converted pixel before placing the event name beside it.
- Save MapPanel.java.
- Switch back to the terminal.
- Compile the event renderer by running:
mvn compile
What does this compile confirm?
Maven confirms that MapPanel can call the longitude, latitude, and name getters on every GeoEvent.
Does the marker method fail?
Confirm the getter names match getLongitude(), getLatitude(), and getName() exactly.
Help me debug the drawEvent method.
The panel has all its drawing helpers. Its painting override now needs to call them whenever Swing refreshes the window.
- In MapPanel.java, find the current painting override:
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
}
What does the current override do?
The current method only clears the panel through JPanel. The drawing helpers exist but are not called from the painting cycle yet.
- Replace the current override with this completed version:
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D graphics2D = (Graphics2D) graphics.create();
graphics2D.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(graphics2D);
for (GeoEvent event : events) {
drawEvent(graphics2D, event);
}
graphics2D.dispose();
}
How does the painting cycle work?
- The copied graphics context keeps this panel's settings isolated.
- The rendering hint smooths marker edges and text.
- The event loop paints every object held by the panel.
- The final call releases the copied graphics context.
- Save MapPanel.java.
- Switch back to the terminal.
- Compile the complete panel by running:
mvn compile
What does the completed compile prove?
The successful compile checks the panel shell and every drawing helper as one class. Your visual layer is ready for an application entry point.
Does the painting override fail?
Confirm that you replaced the old override instead of keeping two copies. Check that drawGrid() and drawEvent() remain inside the class.
Help me fix my paintComponent override.
✔️ Awesome, I've got everything!
Your grid and marker renderer compile together. Save MapPanel.java before you create the entry point.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.List;
import javax.swing.JPanel;
public class MapPanel extends JPanel {
private static final double MIN_LATITUDE = 39.0;
private static final double MAX_LATITUDE = 40.6;
private static final double MIN_LONGITUDE = -105.8;
private static final double MAX_LONGITUDE = -104.1;
private static final int PADDING = 50;
private final List<GeoEvent> events;
public MapPanel(List<GeoEvent> events) {
this.events = events;
setPreferredSize(new Dimension(900, 550));
setBackground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D graphics2D = (Graphics2D) graphics.create();
graphics2D.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(graphics2D);
for (GeoEvent event : events) {
drawEvent(graphics2D, event);
}
graphics2D.dispose();
}
private void drawGrid(Graphics2D graphics2D) {
int plotWidth = getWidth() - 2 * PADDING;
int plotHeight = getHeight() - 2 * PADDING;
graphics2D.setColor(new Color(225, 225, 225));
double[] longitudes = {-105.5, -105.0, -104.5};
for (double longitude : longitudes) {
int x = toX(longitude);
graphics2D.drawLine(x, PADDING, x, PADDING + plotHeight);
graphics2D.drawString(longitude + "°", x - 20, PADDING + plotHeight + 20);
}
double[] latitudes = {39.0, 39.5, 40.0, 40.5};
for (double latitude : latitudes) {
int y = toY(latitude);
graphics2D.drawLine(PADDING, y, PADDING + plotWidth, y);
graphics2D.drawString(latitude + "°", 8, y + 5);
}
graphics2D.setColor(Color.DARK_GRAY);
graphics2D.drawRect(PADDING, PADDING, plotWidth, plotHeight);
graphics2D.drawString("Loaded events: " + events.size(), PADDING, 22);
}
private void drawEvent(Graphics2D graphics2D, GeoEvent event) {
graphics2D.setColor(new Color(100, 116, 139));
int x = toX(event.getLongitude());
int y = toY(event.getLatitude());
graphics2D.fillOval(x - 6, y - 6, 12, 12);
graphics2D.drawString(event.getName(), x + 8, y - 8);
}
private int toX(double longitude) {
double fraction = (longitude - MIN_LONGITUDE)
/ (MAX_LONGITUDE - MIN_LONGITUDE);
return PADDING + (int) Math.round(fraction * (getWidth() - 2 * PADDING));
}
private int toY(double latitude) {
double fraction = (MAX_LATITUDE - latitude)
/ (MAX_LATITUDE - MIN_LATITUDE);
return PADDING + (int) Math.round(fraction * (getHeight() - 2 * PADDING));
}
}
Launch and inspect the feed
The application entry point creates the event list before handing it to the panel. Swing schedules the window creation on its graphical event thread.
- In the Explorer sidebar, select the src/main/java/com/nextwork/geo folder.
- Click the New File icon.
- Enter App.java as the file name.
- Create the five-event feed and window by pasting this code:
package com.nextwork.geo;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class App {
public static void main(String[] args) {
List<GeoEvent> events = new ArrayList<>();
events.add(new GeoEvent("North Ridge Fire", "FIRE", 5, 39.95, -105.10));
events.add(new GeoEvent("River Sensor", "FLOOD", 3, 39.70, -104.80));
events.add(new GeoEvent("Grid Alert", "UTILITY", 4, 40.30, -105.50));
events.add(new GeoEvent("South Relay", "COMMUNICATION", 2, 39.20, -104.30));
events.add(new GeoEvent("Bad Coordinate", "UNKNOWN", 1, 95.0, -104.90));
SwingUtilities.invokeLater(() -> showWindow(events));
}
private static void showWindow(List<GeoEvent> events) {
JFrame frame = new JFrame("GeoWatch Operations Analyzer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MapPanel(events));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
What does the entry point do?
- The ArrayList stores five GeoEvent objects in insertion order.
- Four events use coordinates inside the panel's regional range.
- The fifth event is supplied with latitude 95.0.
- The window contains one MapPanel backed by the complete event list.
- Save App.java.
- Switch back to the terminal from earlier.
- Compile the complete application by running:
mvn compile
What does the application compile prove?
Maven compiles the event model and visual panel with the new entry point. A successful finish confirms that the three classes connect correctly.
Does App fail to compile?
Confirm that App.java, GeoEvent.java, and MapPanel.java share the same package declaration.
Check that each event constructor supplies a name, category, severity, latitude, and longitude in that order.
Help me fix my App compilation error.
✔️ Awesome, I've got everything!
Your three application classes now compile together. Keep the graphical desktop session active for the launch.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class App {
public static void main(String[] args) {
List<GeoEvent> events = new ArrayList<>();
events.add(new GeoEvent("North Ridge Fire", "FIRE", 5, 39.95, -105.10));
events.add(new GeoEvent("River Sensor", "FLOOD", 3, 39.70, -104.80));
events.add(new GeoEvent("Grid Alert", "UTILITY", 4, 40.30, -105.50));
events.add(new GeoEvent("South Relay", "COMMUNICATION", 2, 39.20, -104.30));
events.add(new GeoEvent("Bad Coordinate", "UNKNOWN", 1, 95.0, -104.90));
SwingUtilities.invokeLater(() -> showWindow(events));
}
private static void showWindow(List<GeoEvent> events) {
JFrame frame = new JFrame("GeoWatch Operations Analyzer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MapPanel(events));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
Before you launch it, do you expect the loaded count to match the number of markers inside the regional grid?
- Launch GeoWatch from the graphical Fedora or RHEL desktop session by running:
java -cp target/classes com.nextwork.geo.App
What should you see?
- You should see a window titled GeoWatch Operations Analyzer.
- You should see Loaded events: 5 above the regional grid.
- You should count four event markers inside the grid.
- The latitude 95.0 maps above the panel's visible latitude range of 39.0 through 40.6.
Your desktop pipeline works. Better still, the visible mismatch has exposed a real weakness in the event model.
Does the GeoWatch window stay hidden?
Confirm that you launched the application from the graphical Fedora or RHEL desktop session. A terminal reached only through a headless remote session cannot display the Swing window.
Check the terminal for a class-loading or display-related error if the process starts without showing the window.
Help me diagnose why my Swing window does not open.
- Close the GeoWatch window to return control to the terminal.
- Save the plotted feed as a Git milestone by running:
git add .
git commit -m "feat: plot fictional event feed"
What do these Git commands record?
The first command stages the three Java files. The second command records the flawed feed as a named milestone on the main branch.
Is the new commit missing files?
Confirm that all three Java files were saved before staging. Run the commit command again after saving any file that remained unsaved.
Help me check why my plotted feed commit is incomplete.
Before you inspect the history, how many milestone commits do you expect to see?
- Verify the project history by running:
git log --oneline
What confirms this step is complete?
You should see feat: plot fictional event feed above chore: initialize Maven project. Those two entries confirm that the plotted feed and Maven foundation are both preserved.
You now have a runnable desktop plot and a concrete data flaw you can explain. Next up, you will protect the event model from impossible coordinates while keeping the rest of the feed alive.
Protect the Model with Coordinate Validation
The Swing plot from the last step counted five events while drawing only four markers. That missing marker proves the feed accepted a coordinate that cannot represent a real latitude.
This step makes the Java model protect its own coordinate rules. You will keep the feed running when one event is rejected.
You will also use JUnit to prove that invalid values fail while boundary values remain valid.
In this step, get ready to:
- Build coordinate rejection into the event model.
- Keep the fictional event feed running after one event is rejected.
- Lock the behavior into a tested Git milestone.
Reject impossible coordinates
A coordinate invariant is a rule that every event must satisfy. Enforcing the rule in the constructor prevents an invalid event from existing anywhere in the application.
Why validate inside the constructor?
Every part of the application creates events through the same constructor. One validation path therefore protects the plot plus every spatial operation you add later.
- Select src/main/java/com/nextwork/geo in the VS Code Explorer sidebar.
- Click New File in the Explorer toolbar.
- Enter InvalidCoordinateException.java in the name field.
- Press Enter.
You will see InvalidCoordinateException.java inside the existing com/nextwork/geo package.
- Define the custom exception by pasting this class into InvalidCoordinateException.java:
package com.nextwork.geo;
public class InvalidCoordinateException extends IllegalArgumentException {
public InvalidCoordinateException(String message) {
super(message);
}
}
What does this class do?
- Extending IllegalArgumentException makes this an unchecked exception for an invalid constructor argument.
- The constructor passes a specific coordinate message to the parent exception.
- The dedicated class lets App catch coordinate failures without hiding unrelated problems.
- Save InvalidCoordinateException.java.
- Confirm the new exception compiles by running:
mvn test
What does this check prove?
Maven compiles the application before reaching the test phase. A successful build confirms that the new exception belongs to the correct package.
You should see BUILD SUCCESS. The project still has no test classes at this point.
Does the exception fail to compile?
Check that the filename uses the exact capitalization shown above. Confirm that the package line is package com.nextwork.geo;.
Ask for help with the package or class declaration if the build still fails: help me diagnose why InvalidCoordinateException.java does not compile in my Maven project
✔️ Awesome, I've got everything!
Your custom coordinate exception is in the correct package.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
public class InvalidCoordinateException extends IllegalArgumentException {
public InvalidCoordinateException(String message) {
super(message);
}
}
How to use this reference
Compare the package declaration plus both class names with your file. Every character should match before you continue.
The exception now gives the model a precise failure type. The next change decides which coordinate values trigger it.
- Select GeoEvent.java in the VS Code Explorer sidebar.
- Replace the current constructor section with this validated constructor section:
public GeoEvent(
String name,
String category,
int severity,
double latitude,
double longitude) {
validateCoordinates(latitude, longitude);
this.name = name;
this.category = category;
this.severity = severity;
this.latitude = latitude;
this.longitude = longitude;
}
private static void validateCoordinates(double latitude, double longitude) {
if (!Double.isFinite(latitude) || latitude < -90.0 || latitude > 90.0) {
throw new InvalidCoordinateException(
"Latitude must be finite and between -90 and 90.");
}
if (!Double.isFinite(longitude) || longitude < -180.0 || longitude > 180.0) {
throw new InvalidCoordinateException(
"Longitude must be finite and between -180 and 180.");
}
}
How does coordinate validation work?
- The constructor calls validateCoordinates() before assigning any fields. An invalid event therefore never reaches a completed state.
- Double.isFinite() rejects values such as positive infinity plus NaN.
- Latitude includes every value from -90.0 through 90.0.
- Longitude includes every value from -180.0 through 180.0.
- Save GeoEvent.java.
Before you run the build, do you expect constructor validation to cause a compilation failure?
- Check the validated model by running:
mvn test
Why does the build still pass?
The invalid coordinate becomes a runtime concern when the application constructs Bad Coordinate. The source code remains valid Java, so Maven can compile it successfully.
You should see BUILD SUCCESS. The constructor now owns the coordinate rules.
Does GeoEvent fail to compile?
Confirm that validateCoordinates(latitude, longitude); appears before the field assignments. Check that both closing braces remain after the validation method.
Ask for help locating a missing brace or method if needed: help me compare my GeoEvent constructor and validateCoordinates method with the required structure
✔️ Awesome, I've got everything!
Your event model now rejects non-finite values plus coordinates outside the accepted ranges.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
public class GeoEvent {
private final String name;
private final String category;
private final int severity;
private final double latitude;
private final double longitude;
public GeoEvent(
String name,
String category,
int severity,
double latitude,
double longitude) {
validateCoordinates(latitude, longitude);
this.name = name;
this.category = category;
this.severity = severity;
this.latitude = latitude;
this.longitude = longitude;
}
private static void validateCoordinates(double latitude, double longitude) {
if (!Double.isFinite(latitude) || latitude < -90.0 || latitude > 90.0) {
throw new InvalidCoordinateException(
"Latitude must be finite and between -90 and 90.");
}
if (!Double.isFinite(longitude) || longitude < -180.0 || longitude > 180.0) {
throw new InvalidCoordinateException(
"Longitude must be finite and between -180 and 180.");
}
}
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public int getSeverity() {
return severity;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
}
How to use this reference
Compare the constructor order plus both validation branches with your file. Keep all five existing getters below the new method.
Keep the event feed running
Constructor validation now rejects Bad Coordinate before it enters the event list. The application needs a narrow recovery point so one bad record does not stop the remaining feed.
- Select App.java in the VS Code Explorer sidebar.
- Replace the five direct event insertions with these helper calls:
addEvent(events, "North Ridge Fire", "FIRE", 5, 39.95, -105.10);
addEvent(events, "River Sensor", "FLOOD", 3, 39.70, -104.80);
addEvent(events, "Grid Alert", "UTILITY", 4, 40.30, -105.50);
addEvent(events, "South Relay", "COMMUNICATION", 2, 39.20, -104.30);
addEvent(events, "Bad Coordinate", "UNKNOWN", 1, 95.0, -104.90);
Why route every event through one helper?
Each feed entry now follows the same creation path. The helper can accept valid events while handling rejected coordinates consistently.
- Add this helper below the closing brace of main():
private static void addEvent(
List<GeoEvent> events,
String name,
String category,
int severity,
double latitude,
double longitude) {
try {
events.add(new GeoEvent(name, category, severity, latitude, longitude));
} catch (InvalidCoordinateException exception) {
System.out.println("Rejected event: " + exception.getMessage());
}
}
How does the helper protect the feed?
- The try block creates the event before adding it to the list.
- The catch block handles only InvalidCoordinateException.
- A rejected event produces a terminal report while the remaining events continue to the plot.
- Save App.java.
- Compile the updated application by running:
mvn test
What does this build confirm?
Maven compiles the updated helper plus its exception handling path. A successful build confirms that App can call the new method.
Before you launch the feed, do you expect the bad event to stop the application or be reported while the window opens?
- Launch the corrected event feed by running:
java -cp target/classes com.nextwork.geo.App
What changed in the running application?
The constructor rejects the latitude of 95.0. The helper catches that rejection before the invalid event reaches MapPanel.
The valid events still reach the Swing window. The model and application now cooperate at a clear failure boundary.
That data-quality gap is closed. Your terminal prints Rejected event: Latitude must be finite and between -90 and 90. while the window reports Loaded events: 4.
You will see four event markers inside the regional grid. The rejected coordinate no longer contributes to the loaded count.
Does the feed stop before the window opens?
Check that every event uses addEvent(). A remaining direct new GeoEvent call can let the exception escape from main().
Confirm that the launch runs from a graphical Fedora or RHEL desktop session if the project compiles but no window opens.
Ask for help tracing the creation path if the application still stops: help me find why InvalidCoordinateException escapes from App instead of being caught by addEvent
- Close the GeoWatch window to return control to the terminal.
Prove the coordinate rules
The corrected plot proves one example works. Automated tests protect the full contract by checking invalid latitude, non-finite longitude, plus valid boundary coordinates.
- Select src/test/java/com/nextwork/geo in the VS Code Explorer sidebar.
- Click New File in the Explorer toolbar.
- Enter GeoEventTest.java in the name field.
- Press Enter.
Start with two rejection tests. Each test creates one deliberately invalid event inside an exception assertion.
- Add the first two coordinate tests by pasting this code into GeoEventTest.java:
package com.nextwork.geo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class GeoEventTest {
@Test
void rejectsLatitudeOutsideRange() {
assertThrows(
InvalidCoordinateException.class,
() -> new GeoEvent("Invalid", "TEST", 1, 91.0, 0.0));
}
@Test
void rejectsNonFiniteLongitude() {
assertThrows(
InvalidCoordinateException.class,
() -> new GeoEvent("Invalid", "TEST", 1, 0.0, Double.NaN));
}
}
What do these tests prove?
- The first test expects InvalidCoordinateException when latitude is 91.0.
- The second test passes Double.NaN as longitude to exercise the finite-number rule.
- assertThrows() fails the test if construction succeeds or throws a different exception type.
- Save GeoEventTest.java.
How many tests do you expect Maven to report from the methods you just added?
- Run the first pair of tests with:
mvn test
What does the first test run establish?
Maven discovers methods marked with @Test through the configured test plugin. Both rejection paths now have executable proof.
Maven reports two passing tests. This confirms that out-of-range latitude plus non-finite longitude are rejected.
Are the rejection tests failing?
Confirm that the first test uses latitude 91.0. Confirm that the second test places Double.NaN in the longitude position.
Ask for help comparing the assertions with the constructor rules: help me diagnose why my GeoEvent assertThrows tests do not pass
Rejection is only half of the contract. The final test proves that the exact geographic limits remain valid.
- Place your cursor before the final closing brace in GeoEventTest.java.
- Insert this boundary test:
@Test
void acceptsBoundaryCoordinates() {
GeoEvent event = new GeoEvent("Boundary", "TEST", 1, -90.0, 180.0);
assertEquals(-90.0, event.getLatitude());
assertEquals(180.0, event.getLongitude());
}
Why test the exact boundaries?
The accepted ranges are inclusive. Testing -90.0 plus 180.0 catches an accidental strict comparison at either edge.
The getter assertions also prove that valid boundary values survive construction unchanged.
- Save GeoEventTest.java.
Before you run the complete suite, how many tests should Maven discover now?
- Run all coordinate tests with:
mvn test
What does the completed suite prove?
The suite covers an out-of-range value plus a non-finite value. It also protects the inclusive coordinate boundaries.
These tests make the constructor contract repeatable from the terminal. Future model changes must preserve the same behavior.
Your first automated suite is passing. Maven reports three successful tests for the coordinate rules.
Does the boundary test fail?
Check that the latitude comparisons use < -90.0 plus > 90.0. Check that the longitude comparisons use < -180.0 plus > 180.0.
Ask for help if Maven discovers the test but an assertion still fails: help me diagnose why GeoEvent rejects an inclusive coordinate boundary
✔️ Awesome, I've got everything!
Your test class now covers both rejection paths plus accepted boundary coordinates.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class GeoEventTest {
@Test
void rejectsLatitudeOutsideRange() {
assertThrows(
InvalidCoordinateException.class,
() -> new GeoEvent("Invalid", "TEST", 1, 91.0, 0.0));
}
@Test
void rejectsNonFiniteLongitude() {
assertThrows(
InvalidCoordinateException.class,
() -> new GeoEvent("Invalid", "TEST", 1, 0.0, Double.NaN));
}
@Test
void acceptsBoundaryCoordinates() {
GeoEvent event = new GeoEvent("Boundary", "TEST", 1, -90.0, 180.0);
assertEquals(-90.0, event.getLatitude());
assertEquals(180.0, event.getLongitude());
}
}
How to use this reference
Compare all three method names plus their coordinate arguments with your test file. Confirm that the class has one final closing brace.
The constructor rules plus their tests now form one complete milestone. Record that working state before moving into spatial calculations.
- Stage the validated model plus its tests by running these commands:
git add .
git commit -m "test: validate geographic coordinates"
What do these commands record?
- git add . stages the exception, model update, application recovery path, plus test class.
- git commit -m "test: validate geographic coordinates" saves the complete coordinate-validation milestone.
Did the commit fail?
Read the terminal message to identify whether Git needs identity configuration or whether no files were staged. Keep the tested files unchanged while you resolve the repository issue.
Ask for help with the reported commit problem: help me diagnose why my coordinate validation Git commit failed
Before you check the repository, do you expect any uncommitted coordinate-validation changes to remain?
- Verify the saved milestone by running:
git status
What should the repository show?
Git should report a clean working tree on the main branch. That state confirms every validated change belongs to the new commit.
You now have a model that rejects impossible coordinates without sacrificing the rest of the feed.
Your event feed is valid plus tested. Next up, you will turn those trusted coordinates into distance, radius, plus bounding-box results.
Calculate and Visualize Spatial Queries
Your validated event feed now keeps impossible coordinates out of the Swing display. Maven already proves those coordinate rules with three passing tests.
Valid points still cannot answer which events are nearby or inside an operational region. This step adds reusable spatial operations before connecting their results to the terminal report and map colors.
In this step, get ready to:
- Calculate spherical distances between geographic events.
- Filter events with radius and bounding-box queries.
- Prove the results with tests and a color-coded plot.
Create reusable spatial operations
A bounding box checks whether a point lies inside fixed latitude and longitude limits. Inclusive comparisons also count points that sit directly on an edge.
The Haversine formula estimates great-circle distance across a spherical Earth. GeoWatch uses a radius of 6371.0 kilometers before clamping the intermediate value for a stable inverse trigonometric calculation.
- Select the src/main/java/com/nextwork/geo package in the VS Code Explorer sidebar.
- Create BoundingBox.java with this first runnable version:
package com.nextwork.geo;
public class BoundingBox {
private final double minLatitude;
private final double maxLatitude;
private final double minLongitude;
private final double maxLongitude;
public BoundingBox(
double minLatitude,
double maxLatitude,
double minLongitude,
double maxLongitude) {
this.minLatitude = minLatitude;
this.maxLatitude = maxLatitude;
this.minLongitude = minLongitude;
this.maxLongitude = maxLongitude;
}
public boolean contains(GeoEvent event) {
return event.getLatitude() >= minLatitude
&& event.getLatitude() <= maxLatitude
&& event.getLongitude() >= minLongitude
&& event.getLongitude() <= maxLongitude;
}
}
What does this code do?
- The four fields store the box edges after construction.
- The contains() method checks both coordinates against those edges.
- The comparison operators include points that sit exactly on the minimum or maximum boundary.
- Save BoundingBox.java.
- Confirm the new class compiles with the existing project by running:
mvn test
What does this check prove?
Maven compiles the new class before running the existing coordinate tests. You should still see three passing tests followed by BUILD SUCCESS.
Your first spatial predicate now compiles without disturbing the protected event model.
- In the BoundingBox constructor, place these checks before the field assignments:
validateRange(minLatitude, maxLatitude, -90.0, 90.0, "latitude");
validateRange(minLongitude, maxLongitude, -180.0, 180.0, "longitude");
Why validate both ranges?
Each call supplies the allowed geographic limits for one coordinate axis. The helper also checks that the minimum comes before the maximum.
- Place this helper between the constructor and contains():
private static void validateRange(
double minimum,
double maximum,
double allowedMinimum,
double allowedMaximum,
String label) {
if (!Double.isFinite(minimum)
|| !Double.isFinite(maximum)
|| minimum < allowedMinimum
|| maximum > allowedMaximum
|| minimum > maximum) {
throw new InvalidCoordinateException("Invalid " + label + " range.");
}
}
How does the guard work?
- The finite checks reject undefined or infinite edges.
- The geographic checks keep latitude inside -90.0 through 90.0 and longitude inside -180.0 through 180.0.
- The ordering check rejects an inverted range before it reaches a query.
- Save BoundingBox.java.
- Verify the completed range validation by running:
mvn test
What should this confirm?
Maven should compile the validated bounding box and keep all three existing tests green. A successful run confirms the new guard integrates with InvalidCoordinateException.
- Create GeoAnalyzer.java in the same package with the distance calculation below:
package com.nextwork.geo;
import java.util.ArrayList;
import java.util.List;
public class GeoAnalyzer {
private static final double EARTH_RADIUS_KM = 6371.0;
public double distanceKm(GeoEvent first, GeoEvent second) {
double latitude1 = Math.toRadians(first.getLatitude());
double latitude2 = Math.toRadians(second.getLatitude());
double latitudeDifference = latitude2 - latitude1;
double longitudeDifference = Math.toRadians(
second.getLongitude() - first.getLongitude());
double latitudeTerm = Math.sin(latitudeDifference / 2.0);
double longitudeTerm = Math.sin(longitudeDifference / 2.0);
double haversine = latitudeTerm * latitudeTerm
+ Math.cos(latitude1)
* Math.cos(latitude2)
* longitudeTerm
* longitudeTerm;
double clamped = Math.min(1.0, Math.max(0.0, haversine));
double centralAngle = 2.0 * Math.atan2(
Math.sqrt(clamped),
Math.sqrt(1.0 - clamped));
return EARTH_RADIUS_KM * centralAngle;
}
}
How does the distance calculation work?
- The coordinate conversion moves latitude and longitude from degrees into radians.
- The Haversine terms describe the central angle between the two events.
- The clamp keeps floating-point rounding inside the valid interval from 0.0 through 1.0.
- Multiplying the central angle by EARTH_RADIUS_KM produces the spherical distance in kilometers.
- Save GeoAnalyzer.java.
- Confirm the distance method compiles by running:
mvn test
What does this build check prove?
The successful build proves every Java math operation and event getter resolves correctly. The three coordinate tests should continue to pass.
- Place the radius query below distanceKm():
public List<GeoEvent> withinRadius(
List<GeoEvent> events,
GeoEvent center,
double radiusKm) {
if (!Double.isFinite(radiusKm) || radiusKm < 0.0) {
throw new IllegalArgumentException("Radius must be finite and non-negative.");
}
List<GeoEvent> matches = new ArrayList<>();
for (GeoEvent event : events) {
if (distanceKm(center, event) <= radiusKm) {
matches.add(event);
}
}
return matches;
}
How does the radius query work?
- The first guard rejects negative or non-finite radii.
- The loop measures every event from the chosen center.
- The result includes events whose distance is equal to the requested radius.
- Place the bounding-box query below withinRadius():
public List<GeoEvent> withinBounds(
List<GeoEvent> events,
BoundingBox boundingBox) {
List<GeoEvent> matches = new ArrayList<>();
for (GeoEvent event : events) {
if (boundingBox.contains(event)) {
matches.add(event);
}
}
return matches;
}
How does the bounds query work?
The method delegates each containment decision to BoundingBox.contains(). This keeps the range predicate in one reusable class.
- Save GeoAnalyzer.java.
- Verify all three analyzer operations compile by running:
mvn test
What should Maven report?
Maven should report three passing tests because the analyzer tests arrive in the next substep. BUILD SUCCESS confirms both new production classes compile.
Seeing a compilation problem?
Check that both files start with the package declaration for com.nextwork.geo. Confirm every method sits inside its class braces.
Compare the capitalization of GeoEvent, BoundingBox, and InvalidCoordinateException with the existing filenames.
Help me find the compilation issue in my new spatial classes.
✔️ Awesome, I've got everything!
Great. Save both spatial classes before moving on to their tests.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
public class BoundingBox {
private final double minLatitude;
private final double maxLatitude;
private final double minLongitude;
private final double maxLongitude;
public BoundingBox(
double minLatitude,
double maxLatitude,
double minLongitude,
double maxLongitude) {
validateRange(minLatitude, maxLatitude, -90.0, 90.0, "latitude");
validateRange(minLongitude, maxLongitude, -180.0, 180.0, "longitude");
this.minLatitude = minLatitude;
this.maxLatitude = maxLatitude;
this.minLongitude = minLongitude;
this.maxLongitude = maxLongitude;
}
private static void validateRange(
double minimum,
double maximum,
double allowedMinimum,
double allowedMaximum,
String label) {
if (!Double.isFinite(minimum)
|| !Double.isFinite(maximum)
|| minimum < allowedMinimum
|| maximum > allowedMaximum
|| minimum > maximum) {
throw new InvalidCoordinateException("Invalid " + label + " range.");
}
}
public boolean contains(GeoEvent event) {
return event.getLatitude() >= minLatitude
&& event.getLatitude() <= maxLatitude
&& event.getLongitude() >= minLongitude
&& event.getLongitude() <= maxLongitude;
}
}
What should you compare?
Confirm the constructor calls validateRange() before assigning the four fields. Check that contains() uses inclusive comparisons.
package com.nextwork.geo;
import java.util.ArrayList;
import java.util.List;
public class GeoAnalyzer {
private static final double EARTH_RADIUS_KM = 6371.0;
public double distanceKm(GeoEvent first, GeoEvent second) {
double latitude1 = Math.toRadians(first.getLatitude());
double latitude2 = Math.toRadians(second.getLatitude());
double latitudeDifference = latitude2 - latitude1;
double longitudeDifference = Math.toRadians(
second.getLongitude() - first.getLongitude());
double latitudeTerm = Math.sin(latitudeDifference / 2.0);
double longitudeTerm = Math.sin(longitudeDifference / 2.0);
double haversine = latitudeTerm * latitudeTerm
+ Math.cos(latitude1)
* Math.cos(latitude2)
* longitudeTerm
* longitudeTerm;
double clamped = Math.min(1.0, Math.max(0.0, haversine));
double centralAngle = 2.0 * Math.atan2(
Math.sqrt(clamped),
Math.sqrt(1.0 - clamped));
return EARTH_RADIUS_KM * centralAngle;
}
public List<GeoEvent> withinRadius(
List<GeoEvent> events,
GeoEvent center,
double radiusKm) {
if (!Double.isFinite(radiusKm) || radiusKm < 0.0) {
throw new IllegalArgumentException("Radius must be finite and non-negative.");
}
List<GeoEvent> matches = new ArrayList<>();
for (GeoEvent event : events) {
if (distanceKm(center, event) <= radiusKm) {
matches.add(event);
}
}
return matches;
}
public List<GeoEvent> withinBounds(
List<GeoEvent> events,
BoundingBox boundingBox) {
List<GeoEvent> matches = new ArrayList<>();
for (GeoEvent event : events) {
if (boundingBox.contains(event)) {
matches.add(event);
}
}
return matches;
}
}
What should you compare?
Confirm the distance method appears before the two query methods. Check that each query creates and returns its own ArrayList.
Prove the query behavior with JUnit
A distance formula can compile while still producing the wrong number. JUnit gives each spatial rule a small input with a concrete expected result.
- Select the src/test/java/com/nextwork/geo package in the VS Code Explorer sidebar.
- Create GeoAnalyzerTest.java with the equatorial distance test below:
package com.nextwork.geo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class GeoAnalyzerTest {
private final GeoAnalyzer analyzer = new GeoAnalyzer();
@Test
void calculatesOneDegreeAtEquator() {
GeoEvent first = new GeoEvent("First", "TEST", 1, 0.0, 0.0);
GeoEvent second = new GeoEvent("Second", "TEST", 1, 0.0, 1.0);
assertEquals(111.19, analyzer.distanceKm(first, second), 0.05);
}
}
What does this test prove?
The test places two events one degree of longitude apart at the equator. The assertion accepts a difference of up to 0.05 kilometers around the expected spherical result of 111.19 kilometers.
- Save GeoAnalyzerTest.java.
- Run the new distance test with the existing model tests by running:
mvn test
What should this run prove?
Maven should report four passing tests with no failures. The new result proves the distance calculation is close to the expected value.
- Place this radius test below calculatesOneDegreeAtEquator():
@Test
void returnsOnlyEventsWithinRadius() {
GeoEvent center = new GeoEvent("Center", "TEST", 1, 0.0, 0.0);
List<GeoEvent> events = new ArrayList<>();
events.add(new GeoEvent("Near", "TEST", 1, 0.0, 0.1));
events.add(new GeoEvent("Far", "TEST", 1, 0.0, 1.0));
List<GeoEvent> matches = analyzer.withinRadius(events, center, 20.0);
assertEquals(1, matches.size());
assertEquals("Near", matches.get(0).getName());
}
What does the radius test prove?
The nearby event lies inside a 20.0 kilometer radius. The farther event stays out of the returned list.
- Save GeoAnalyzerTest.java.
- Verify the radius filter by running:
mvn test
What should this run prove?
Maven should now report five passing tests. The additional test confirms withinRadius() returns only the matching event.
- Place this edge-containment test below returnsOnlyEventsWithinRadius():
@Test
void includesBoundingBoxEdges() {
BoundingBox box = new BoundingBox(-1.0, 1.0, -1.0, 1.0);
List<GeoEvent> events = new ArrayList<>();
events.add(new GeoEvent("Inside", "TEST", 1, 0.0, 0.0));
events.add(new GeoEvent("Edge", "TEST", 1, 1.0, 1.0));
events.add(new GeoEvent("Outside", "TEST", 1, 2.0, 0.0));
List<GeoEvent> matches = analyzer.withinBounds(events, box);
assertEquals(2, matches.size());
assertEquals("Edge", matches.get(1).getName());
}
What does the edge test prove?
The expected result includes the interior point and the point at 1.0, 1.0. This proves the maximum latitude and longitude edges are inclusive.
- Save GeoAnalyzerTest.java.
Before you run the complete suite, how many tests do you expect Maven to execute now?
- Test all coordinate and spatial behavior by running:
mvn test
What should Maven report?
Maven should report six passing tests with no failures. Three protect the event model while three prove the spatial calculations.
That is a strong checkpoint. Your analyzer now has automated evidence for distance, radius, and inclusive boundary behavior.
One of the spatial tests failed?
Check the expected distance and tolerance in calculatesOneDegreeAtEquator(). Confirm the radius query compares distance with <=.
For the bounds test, verify every comparison in contains() includes equality.
Help me diagnose the failing spatial test.
✔️ Awesome, I've got everything!
Your six-test suite is ready. Save the test file before wiring the results into the application.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class GeoAnalyzerTest {
private final GeoAnalyzer analyzer = new GeoAnalyzer();
@Test
void calculatesOneDegreeAtEquator() {
GeoEvent first = new GeoEvent("First", "TEST", 1, 0.0, 0.0);
GeoEvent second = new GeoEvent("Second", "TEST", 1, 0.0, 1.0);
assertEquals(111.19, analyzer.distanceKm(first, second), 0.05);
}
@Test
void returnsOnlyEventsWithinRadius() {
GeoEvent center = new GeoEvent("Center", "TEST", 1, 0.0, 0.0);
List<GeoEvent> events = new ArrayList<>();
events.add(new GeoEvent("Near", "TEST", 1, 0.0, 0.1));
events.add(new GeoEvent("Far", "TEST", 1, 0.0, 1.0));
List<GeoEvent> matches = analyzer.withinRadius(events, center, 20.0);
assertEquals(1, matches.size());
assertEquals("Near", matches.get(0).getName());
}
@Test
void includesBoundingBoxEdges() {
BoundingBox box = new BoundingBox(-1.0, 1.0, -1.0, 1.0);
List<GeoEvent> events = new ArrayList<>();
events.add(new GeoEvent("Inside", "TEST", 1, 0.0, 0.0));
events.add(new GeoEvent("Edge", "TEST", 1, 1.0, 1.0));
events.add(new GeoEvent("Outside", "TEST", 1, 2.0, 0.0));
List<GeoEvent> matches = analyzer.withinBounds(events, box);
assertEquals(2, matches.size());
assertEquals("Edge", matches.get(1).getName());
}
}
What should you compare?
Confirm the class contains exactly three test methods. Check the expected distance, radius, result sizes, and edge event name carefully.
Connect results to the Swing plot
The query lists answer operational questions in the terminal. Java 2D turns the same memberships into visible categories on the plot.
How will the color legend work?
- Purple marks an event returned by both queries.
- Red marks a radius-only event.
- Blue marks a bounding-box-only event.
- Gray marks an unmatched event while orange marks the command center.
- In App.java, place this query setup after the five addEvent() calls:
GeoEvent center = new GeoEvent(
"Command Center", "REFERENCE", 1, 39.7392, -104.9903);
GeoAnalyzer analyzer = new GeoAnalyzer();
double radiusKm = 35.0;
BoundingBox boundingBox = new BoundingBox(39.9, 40.4, -105.6, -105.0);
List<GeoEvent> radiusMatches = analyzer.withinRadius(
events, center, radiusKm);
List<GeoEvent> boundingMatches = analyzer.withinBounds(
events, boundingBox);
What does this setup define?
- The command center becomes the reference point for every distance and radius check.
- The 35.0 kilometer radius selects nearby events.
- The bounding box describes the latitude and longitude region used by the second query.
- Place this report after the two match lists and before the current Swing window launch:
System.out.println("\nDistance from Command Center");
for (GeoEvent event : events) {
System.out.printf(
"%-18s %-14s severity=%d distance=%6.2f km%n",
event.getName(),
event.getCategory(),
event.getSeverity(),
analyzer.distanceKm(center, event));
}
printMatches("Inside 35 km", radiusMatches);
printMatches("Inside bounding box", boundingMatches);
What does the report show?
The loop prints each valid event with its category, severity, and distance from the command center. The two headings separate radius matches from bounding-box matches.
- Place this helper between addEvent() and the current window method:
private static void printMatches(String heading, List<GeoEvent> matches) {
System.out.println("\n" + heading);
for (GeoEvent event : matches) {
System.out.println("- " + event.getName());
}
}
Why use a reporting helper?
Both queries need the same heading-and-list format. printMatches() keeps that terminal formatting in one place.
- Save App.java.
- Run the application to inspect the new terminal report:
mvn test
java -cp target/classes com.nextwork.geo.App
What should you see now?
Maven should keep all six tests green. The terminal should list North Ridge Fire and River Sensor under Inside 35 km.
The current window still uses its earlier marker rendering. The terminal output proves the query layer works before the display consumes those lists.
- Close the GeoWatch window to return control to the terminal.
- In MapPanel.java, replace the grid constants, event fields, and constructor section with:
private static final double MIN_LATITUDE = 39.0;
private static final double MAX_LATITUDE = 40.6;
private static final double MIN_LONGITUDE = -105.8;
private static final double MAX_LONGITUDE = -104.1;
private static final int PADDING = 50;
private final List<GeoEvent> allEvents;
private final List<GeoEvent> radiusMatches;
private final List<GeoEvent> boundingMatches;
private final GeoEvent referenceEvent;
public MapPanel(
List<GeoEvent> allEvents,
List<GeoEvent> radiusMatches,
List<GeoEvent> boundingMatches,
GeoEvent referenceEvent) {
this.allEvents = allEvents;
this.radiusMatches = radiusMatches;
this.boundingMatches = boundingMatches;
this.referenceEvent = referenceEvent;
setPreferredSize(new Dimension(900, 550));
setBackground(Color.WHITE);
}
Why does the panel need four inputs?
The panel needs the complete feed for plotting and the two match lists for classification. The reference event gets its own orange marker.
- Replace the current paintComponent() method with:
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D graphics2D = (Graphics2D) graphics.create();
graphics2D.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(graphics2D);
for (GeoEvent event : allEvents) {
drawEvent(graphics2D, event);
}
drawReference(graphics2D);
drawLegend(graphics2D);
graphics2D.dispose();
}
What changes in the painting order?
The panel draws the grid first and places event markers over it. The command center and legend are added after the event loop.
- Replace the current drawGrid() method with:
private void drawGrid(Graphics2D graphics2D) {
int plotWidth = getWidth() - 2 * PADDING;
int plotHeight = getHeight() - 2 * PADDING;
graphics2D.setColor(new Color(225, 225, 225));
double[] longitudes = {-105.5, -105.0, -104.5};
for (double longitude : longitudes) {
int x = toX(longitude);
graphics2D.drawLine(x, PADDING, x, PADDING + plotHeight);
graphics2D.drawString(longitude + "°", x - 20, PADDING + plotHeight + 20);
}
double[] latitudes = {39.0, 39.5, 40.0, 40.5};
for (double latitude : latitudes) {
int y = toY(latitude);
graphics2D.drawLine(PADDING, y, PADDING + plotWidth, y);
graphics2D.drawString(latitude + "°", 8, y + 5);
}
graphics2D.setColor(Color.DARK_GRAY);
graphics2D.drawRect(PADDING, PADDING, plotWidth, plotHeight);
graphics2D.drawString("Loaded events: " + allEvents.size(), PADDING, 22);
}
What does the grid establish?
The method converts fixed longitude and latitude values into grid lines. The loaded count now comes from allEvents.
- Replace the current drawEvent() method with:
private void drawEvent(Graphics2D graphics2D, GeoEvent event) {
boolean inRadius = radiusMatches.contains(event);
boolean inBounds = boundingMatches.contains(event);
if (inRadius && inBounds) {
graphics2D.setColor(new Color(126, 34, 206));
} else if (inRadius) {
graphics2D.setColor(new Color(220, 38, 38));
} else if (inBounds) {
graphics2D.setColor(new Color(37, 99, 235));
} else {
graphics2D.setColor(new Color(100, 116, 139));
}
int x = toX(event.getLongitude());
int y = toY(event.getLatitude());
graphics2D.fillOval(x - 6, y - 6, 12, 12);
graphics2D.drawString(event.getName(), x + 8, y - 8);
}
How does marker classification work?
Membership in both lists takes priority and produces purple. Each remaining branch handles one query or no query match.
- Place this reference-marker method below drawEvent():
private void drawReference(Graphics2D graphics2D) {
int x = toX(referenceEvent.getLongitude());
int y = toY(referenceEvent.getLatitude());
graphics2D.setColor(new Color(234, 88, 12));
graphics2D.fillRect(x - 7, y - 7, 14, 14);
graphics2D.drawString(referenceEvent.getName(), x + 9, y + 16);
}
Why use a square marker?
The orange square separates the command center from the circular event markers. Its coordinates pass through the same pixel conversion methods.
- Place this legend method below drawReference():
private void drawLegend(Graphics2D graphics2D) {
graphics2D.setColor(Color.DARK_GRAY);
graphics2D.drawString(
"Gray: all Red: radius Blue: bounds Purple: both Orange: center",
PADDING,
getHeight() - 12);
}
What does the legend add?
The legend makes each marker category readable without inspecting the source code. It sits beneath the coordinate grid.
- In App.java, replace the current Swing launch inside main() with:
SwingUtilities.invokeLater(() -> showWindow(
events, radiusMatches, boundingMatches, center));
What does the new launch pass through?
The event-dispatch task now supplies the full feed, both match lists, and the command center to the window method.
- Replace the current showWindow() method with:
private static void showWindow(
List<GeoEvent> events,
List<GeoEvent> radiusMatches,
List<GeoEvent> boundingMatches,
GeoEvent center) {
JFrame frame = new JFrame("GeoWatch Operations Analyzer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MapPanel(events, radiusMatches, boundingMatches, center));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
How does the window complete the connection?
The method passes every query result into the new four-argument MapPanel constructor. The packed frame then displays the classified markers.
- Save MapPanel.java.
- Save App.java.
Before you run the final check, which event do you expect to belong to both query results?
- Run the complete test suite and launch the updated analyzer with:
mvn test
java -cp target/classes com.nextwork.geo.App
What should you see?
Maven should report six passing tests. The terminal should list North Ridge Fire and River Sensor inside 35 km.
The plot should show North Ridge in purple, River Sensor in red, Grid Alert in blue, South Relay in gray, and Command Center in orange.
You have turned the validated feed into an operational display. Every marker color now traces back to a tested spatial rule.
Missing a marker color?
Check that drawEvent() tests the overlap branch before either single-query branch. Confirm the MapPanel constructor assigns both match lists to their fields.
If the window cannot open, run the application from your graphical Fedora or RHEL desktop session.
Help me diagnose the GeoWatch marker colors.
- Close the GeoWatch window to return control to the terminal.
- Save the completed spatial-query milestone by running:
git add .
git commit -m "feat: add tested spatial queries"
What does this commit preserve?
The commit records the analyzer classes, three new tests, terminal reporting, and color-coded rendering as one working milestone.
✔️ Awesome, I've got everything!
Your application files match the tested spatial-query milestone. Keep both files saved before the terminal checkpoint.
ⓧ I'd like to double check the full code
package com.nextwork.geo;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class App {
public static void main(String[] args) {
List<GeoEvent> events = new ArrayList<>();
addEvent(events, "North Ridge Fire", "FIRE", 5, 39.95, -105.10);
addEvent(events, "River Sensor", "FLOOD", 3, 39.70, -104.80);
addEvent(events, "Grid Alert", "UTILITY", 4, 40.30, -105.50);
addEvent(events, "South Relay", "COMMUNICATION", 2, 39.20, -104.30);
addEvent(events, "Bad Coordinate", "UNKNOWN", 1, 95.0, -104.90);
GeoEvent center = new GeoEvent(
"Command Center", "REFERENCE", 1, 39.7392, -104.9903);
GeoAnalyzer analyzer = new GeoAnalyzer();
double radiusKm = 35.0;
BoundingBox boundingBox = new BoundingBox(39.9, 40.4, -105.6, -105.0);
List<GeoEvent> radiusMatches = analyzer.withinRadius(
events, center, radiusKm);
List<GeoEvent> boundingMatches = analyzer.withinBounds(
events, boundingBox);
System.out.println("\nDistance from Command Center");
for (GeoEvent event : events) {
System.out.printf(
"%-18s %-14s severity=%d distance=%6.2f km%n",
event.getName(),
event.getCategory(),
event.getSeverity(),
analyzer.distanceKm(center, event));
}
printMatches("Inside 35 km", radiusMatches);
printMatches("Inside bounding box", boundingMatches);
SwingUtilities.invokeLater(() -> showWindow(
events, radiusMatches, boundingMatches, center));
}
private static void addEvent(
List<GeoEvent> events,
String name,
String category,
int severity,
double latitude,
double longitude) {
try {
events.add(new GeoEvent(name, category, severity, latitude, longitude));
} catch (InvalidCoordinateException exception) {
System.out.println("Rejected event: " + exception.getMessage());
}
}
private static void printMatches(String heading, List<GeoEvent> matches) {
System.out.println("\n" + heading);
for (GeoEvent event : matches) {
System.out.println("- " + event.getName());
}
}
private static void showWindow(
List<GeoEvent> events,
List<GeoEvent> radiusMatches,
List<GeoEvent> boundingMatches,
GeoEvent center) {
JFrame frame = new JFrame("GeoWatch Operations Analyzer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MapPanel(events, radiusMatches, boundingMatches, center));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
What should you compare in App?
Confirm the queries run before the terminal report. Check that the same match lists pass into showWindow().
package com.nextwork.geo;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.List;
import javax.swing.JPanel;
public class MapPanel extends JPanel {
private static final double MIN_LATITUDE = 39.0;
private static final double MAX_LATITUDE = 40.6;
private static final double MIN_LONGITUDE = -105.8;
private static final double MAX_LONGITUDE = -104.1;
private static final int PADDING = 50;
private final List<GeoEvent> allEvents;
private final List<GeoEvent> radiusMatches;
private final List<GeoEvent> boundingMatches;
private final GeoEvent referenceEvent;
public MapPanel(
List<GeoEvent> allEvents,
List<GeoEvent> radiusMatches,
List<GeoEvent> boundingMatches,
GeoEvent referenceEvent) {
this.allEvents = allEvents;
this.radiusMatches = radiusMatches;
this.boundingMatches = boundingMatches;
this.referenceEvent = referenceEvent;
setPreferredSize(new Dimension(900, 550));
setBackground(Color.WHITE);
}
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D graphics2D = (Graphics2D) graphics.create();
graphics2D.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(graphics2D);
for (GeoEvent event : allEvents) {
drawEvent(graphics2D, event);
}
drawReference(graphics2D);
drawLegend(graphics2D);
graphics2D.dispose();
}
private void drawGrid(Graphics2D graphics2D) {
int plotWidth = getWidth() - 2 * PADDING;
int plotHeight = getHeight() - 2 * PADDING;
graphics2D.setColor(new Color(225, 225, 225));
double[] longitudes = {-105.5, -105.0, -104.5};
for (double longitude : longitudes) {
int x = toX(longitude);
graphics2D.drawLine(x, PADDING, x, PADDING + plotHeight);
graphics2D.drawString(longitude + "°", x - 20, PADDING + plotHeight + 20);
}
double[] latitudes = {39.0, 39.5, 40.0, 40.5};
for (double latitude : latitudes) {
int y = toY(latitude);
graphics2D.drawLine(PADDING, y, PADDING + plotWidth, y);
graphics2D.drawString(latitude + "°", 8, y + 5);
}
graphics2D.setColor(Color.DARK_GRAY);
graphics2D.drawRect(PADDING, PADDING, plotWidth, plotHeight);
graphics2D.drawString("Loaded events: " + allEvents.size(), PADDING, 22);
}
private void drawEvent(Graphics2D graphics2D, GeoEvent event) {
boolean inRadius = radiusMatches.contains(event);
boolean inBounds = boundingMatches.contains(event);
if (inRadius && inBounds) {
graphics2D.setColor(new Color(126, 34, 206));
} else if (inRadius) {
graphics2D.setColor(new Color(220, 38, 38));
} else if (inBounds) {
graphics2D.setColor(new Color(37, 99, 235));
} else {
graphics2D.setColor(new Color(100, 116, 139));
}
int x = toX(event.getLongitude());
int y = toY(event.getLatitude());
graphics2D.fillOval(x - 6, y - 6, 12, 12);
graphics2D.drawString(event.getName(), x + 8, y - 8);
}
private void drawReference(Graphics2D graphics2D) {
int x = toX(referenceEvent.getLongitude());
int y = toY(referenceEvent.getLatitude());
graphics2D.setColor(new Color(234, 88, 12));
graphics2D.fillRect(x - 7, y - 7, 14, 14);
graphics2D.drawString(referenceEvent.getName(), x + 9, y + 16);
}
private void drawLegend(Graphics2D graphics2D) {
graphics2D.setColor(Color.DARK_GRAY);
graphics2D.drawString(
"Gray: all Red: radius Blue: bounds Purple: both Orange: center",
PADDING,
getHeight() - 12);
}
private int toX(double longitude) {
double fraction = (longitude - MIN_LONGITUDE)
/ (MAX_LONGITUDE - MIN_LONGITUDE);
return PADDING + (int) Math.round(fraction * (getWidth() - 2 * PADDING));
}
private int toY(double latitude) {
double fraction = (MAX_LATITUDE - latitude)
/ (MAX_LATITUDE - MIN_LATITUDE);
return PADDING + (int) Math.round(fraction * (getHeight() - 2 * PADDING));
}
}
What should you compare in MapPanel?
Confirm the constructor accepts four arguments. Check the marker branch order, RGB values, reference square, and legend text.
Your spatial analyzer is tested, visible, and committed. Next up, you will prove that you can build, diagnose, recover, and publish it entirely from the Linux terminal.
Prove the Build from the Linux Terminal
Your Java analyzer now calculates spatial queries. Its six JUnit tests prove the results shown in the Swing plot.
An IDE can hide build mechanics. It can also hide recovery steps.
This checkpoint uses only the Fedora or RHEL terminal. You will reproduce the build without IDE shortcuts.
In this step, get ready to:
- Reproduce the complete build from the Linux terminal.
- Diagnose a controlled compiler failure.
- Publish the milestone history to GitHub.
Reproduce the build from the terminal
A reproducible Maven build starts from generated output that can be discarded. Rebuilding from that clean state proves your source files contain everything the application needs.
- Close the GeoWatch window that Visual Studio Code started.
- Switch back to the Fedora or RHEL terminal from earlier.
- Remove the previous Maven build output by running this command:
mvn clean
What does Maven clean remove?
The mvn clean command removes the generated target directory. Your source code remains untouched.
Maven reports that the clean phase completed. The previous target build output is gone.
- Rebuild the project and run its tests by running this command:
mvn test
What does the test phase prove?
The test phase compiles the application before it runs the test suite. A successful result proves the source and all six tests work from a clean build.
Maven reports six passing tests. The build completes successfully.
- Package the tested application into a JAR by running this command:
mvn package
What does the package phase add?
The package phase repeats the required earlier lifecycle phases. Maven then creates the project JAR inside target.
You will see BUILD SUCCESS near the end of the Maven output. Your packaged application now exists in target.
Before you launch the analyzer, ask yourself whether the terminal output and plot will match the version you ran from the IDE.
- Launch the compiled application from the terminal by running this command:
java -cp target/classes com.nextwork.geo.App
How does this launch the application?
The -cp target/classes option tells Java where Maven placed the compiled classes. The fully qualified com.nextwork.geo.App name selects the class containing the application entry point.
The terminal reports the rejected coordinate plus the distance and query results. The window shows four events with the purple, red, blue, gray, and orange visual categories.
- Close the GeoWatch window after confirming the colored plot.
Does the terminal run without a window?
Run the command from your graphical Fedora or RHEL desktop session. A terminal connected through a headless remote session cannot create the GeoWatch window.
Confirm the preceding Maven commands completed successfully before launching the application.
Help me troubleshoot why my GeoWatch Swing window does not open from the Linux terminal.
Diagnose a controlled compiler failure
A controlled failure lets you practise reading build evidence while the correct version remains safely committed. The broken line affects only your working copy.
- Append the controlled broken line to src/main/java/com/nextwork/geo/App.java by running this command:
printf '\nBROKEN\n' >> src/main/java/com/nextwork/geo/App.java
What did the command change?
The printf command appends a blank line followed by BROKEN to App.java. The committed version remains available in Git.
Before you run the tests, ask yourself whether Maven can compile the appended line.
- Expose the compiler failure by running this command:
mvn test
This Failure Is Intentional
Maven compiles the application before running the tests. Compilation stops when the Java compiler reaches the standalone BROKEN line.
This is the expected shortfall. You now have a real compiler failure to diagnose.
- Read the first compiler error that points to App.java.
Git records the difference between the committed source and your working copy. Its diagnostic commands show both the affected file and the exact edit.
- Identify the changed file by running this command:
git status
What does Git status reveal?
The git status command reports that src/main/java/com/nextwork/geo/App.java has an unstaged modification. It leaves the file unchanged.
The status output narrows the failure to App.java.
- Inspect the exact source change by running this command:
git diff
What does Git diff reveal?
The git diff command compares your working copy with the tracked version. The added +BROKEN line identifies the cause without changing anything.
The committed index still contains the working App.java source. Restoring that path removes the temporary edit.
- Recover the tracked version of App.java by running this command:
git restore src/main/java/com/nextwork/geo/App.java
What did Git restore?
The command replaces the modified working copy with the version held in Git's index. The temporary BROKEN line disappears.
Before you rerun the tests, ask yourself whether restoring one source file is enough to recover all six tests.
- Prove the recovered source passes again by running this command:
mvn test
What does the recovered test run prove?
Maven recompiles the restored application before running the test suite. Six passing tests prove the controlled edit is gone.
Maven reports six passing tests again. That is the recovery loop complete.
- Confirm the working tree is clean by running this command:
git status
Why Check the Working Tree?
A clean status proves the temporary compiler failure left no source changes behind. Your application matches the last committed milestone.
Git reports no tracked working-tree changes. Your six-test application is back to its committed state.
Publish the milestone history
A remote GitHub repository makes your terminal-built project available beyond this computer. Keeping the remote empty preserves the local milestone history as the source of truth.
- Go to GitHub's repository creation page in your browser.
- Select New repository.
- Set the repository name to geowatch-analyzer.
GitHub now shows geowatch-analyzer as the repository that will be created.
- Leave the README option unselected.
- Leave the license option unselected.
- Leave the .gitignore option unselected.
- Click Create repository.
GitHub opens the empty repository page. The page provides the remote URL needed by your local project.
- Record the repository remote URL here: REMOTE-URL.
- Add the saved remote URL as origin by replacing REMOTE-URL in this command:
git remote add origin REMOTE-URL
What Is Origin?
The name origin is the local label for your GitHub repository URL. Future push commands can use that short label.
- Verify the configured remote by running this command:
git remote -v
What Should the Remote Check Show?
The output lists the URL associated with origin. Confirm it matches the URL from your geowatch-analyzer repository.
Your GitHub remote URL now appears in the terminal output. That proves the local repository points to the correct destination.
GitHub may ask you to authenticate during the first push. Complete the sign-in flow without pasting a password or access token into your project files.
- Publish the local main branch by running this command:
git push -u origin main
What Does the First Push Establish?
The command uploads your local main branch to origin. The upstream link lets later pushes use the tracked remote branch.
The push publishes all four milestone commits. Your local and remote main branches now share the same history.
Does the Push Need Authentication?
Complete the GitHub authentication flow shown by your terminal or browser. Return to the same terminal after GitHub confirms access.
Confirm the remote URL belongs to the empty geowatch-analyzer repository before trying the push again.
Help me troubleshoot why git push cannot publish my main branch to GitHub.
Before the final checkpoint, ask yourself whether the terminal can still prove the history, tests, package, and desktop application after the recovery.
- Run the complete terminal checkpoint with these commands:
git log --oneline
mvn test
mvn package
java -cp target/classes com.nextwork.geo.App
What Does the Final Checkpoint Prove?
- The history command shows the four milestone commits on main.
- The test command recompiles the application and runs all six tests.
- The package command creates the JAR after the tests pass.
- The Java command prints the spatial reports and opens the colored GeoWatch plot.
Your terminal shows four milestone commits followed by six passing tests. Maven finishes packaging with BUILD SUCCESS.
The GeoWatch window opens with four valid event markers and the orange Command Center. The terminal still reports the rejected invalid latitude.
- Close the GeoWatch window after verifying the plot.
- Refresh the geowatch-analyzer repository page on GitHub.
The repository page shows the pushed main branch with your project files and milestone history. This proves the terminal workflow reached GitHub intact.
You have now built, tested, broken, diagnosed, restored, and published GeoWatch entirely from the Linux terminal. Your source is clean and your remote history is ready to demonstrate.
Secret mission
Rank the Nearest Events
Operational teams often need to know which events are closest to a reference point. Extend GeoWatch with a tested ranking method that prints the two nearest events and marks them with gold rings.
Clean Up Your Resources
Clean Up Your Resources
GeoWatch has no paid runtime services, so keeping the project costs $0. Choose whether to keep your work, pause your desktop session, or delete the project entirely.
Resources you used:
- The local geowatch-analyzer Maven project, including its source code, generated target directory, tests, commits, and local Git repository.
- The remote geowatch-analyzer repository on GitHub, including the pushed main branch and nearest-events extension.
Keep everything running
No action is needed. Choose this option if you want to keep GeoWatch ready for demonstrations or further development.
- Keep the local geowatch-analyzer folder so you retain the source code, tests, generated build files, and local commit history.
- Keep the GitHub repository so the pushed main branch remains available online.
Pause - I'll come back to this later
Closing the desktop application releases its memory. Your local files and GitHub repository remain ready for your return.
- Close the GeoWatch window if it is still open.
- Confirm the Fedora or RHEL terminal has returned to a command prompt.
- Close the terminal when you finish your session.
Delete - I don't want to use this again
This option permanently removes your local project and its Git history. Your pushed GitHub copy remains available until you complete the separate repository deletion steps.
- Confirm the main branch in your GitHub repository contains the nearest-events extension.
- Return to the Fedora or RHEL terminal that is currently inside geowatch-analyzer.
- Remove the local geowatch-analyzer folder by running these commands:
cd ..
rm -rf geowatch-analyzer
What Does This Cleanup Do?
The first line moves the terminal into the folder that contains your project. The second line permanently removes the project source, generated files, tests, and local Git history.
A successful removal usually prints no output. Your terminal prompt remains in the parent folder.
Folder Still Present?
Check that the terminal started inside the geowatch-analyzer folder before you ran the cleanup. A permissions problem can also prevent the folder from being removed.
Help me safely remove my local GeoWatch folder.
Repository deletion is permanent. GitHub shows confirmation warnings before it removes the remote copy.
- Open the geowatch-analyzer repository on GitHub.
- Select the Settings tab.
- Select General in the sidebar.
- Scroll to Danger Zone.
- Select Delete this repository.
- Read the permanent effects shown in the confirmation flow.
- Complete the repository confirmation flow.
- Return to your GitHub repositories list.
You should no longer see geowatch-analyzer in your repositories list. Both the local project and remote repository are now removed.
Nice Work!
Nice Work!
You did it! GeoWatch is now a tested Java geospatial analyzer for fictional operations data.
You've learned how to:
- Build an object-oriented event model with constructor-level coordinate validation. Use a custom exception to reject invalid data without stopping the feed.
- Calculate spherical great-circle distance with the Haversine formula. Filter event collections through radius queries. Test inclusive bounding-box predicates.
- Render query categories in Java Swing. Use Java 2D marker colors to make each match visible. Prove the analyzer with seven JUnit tests run by Maven. Recover a deliberate compiler failure with Git. Publish the clean main branch to GitHub.
- Secret Mission: Rank the nearest events in ascending distance order with nearestEvents. Highlight the top two with gold rings.
Ready to quiz yourself?