Java Geospatial Incident Analyzer

Build a Java CLI that validates incident CSV data and reports sector totals.

Introduction

30 Second Summary

Spreadsheets and exported files often contain typos, missing values, or unexpected labels. One bad record can stop a report before useful information reaches the people who need it.

In this project, you will build a local command-line incident analyzer in Java. It turns synthetic latitude and longitude records into a verified sector summary while safely rejecting malformed input.

What You'll Build

Your finished analyzer turns a mixed-quality incident file into a complete terminal report that names the highest-risk geographic sector.

By the end of this project, you'll have:

  • A runnable Maven-built incident analyzer that turns synthetic CSV records into totals across four geographic sectors.
  • Record-level exception handling that flags malformed coordinates plus unsupported severity values while valid records still produce a full summary.
  • An automated verification suite that ends with a clear pass message when every requirement holds.
  • Secret Mission: Add a severity-level report that totals accepted incidents as LOW, MEDIUM, or HIGH.

Are there any prerequisites?

You need a Mac with Visual Studio Code, internet access, and permission to install local development tools. The setup includes an official fallback for Macs older than macOS Sonoma 14.

Before We Start

A clear scope keeps your first Java project focused on reliable data processing when malformed records appear. Use this checkpoint to commit to a local geospatial incident analyzer for CSV incident records.

Prepare Your Mac for Java

Visual Studio Code gives you an editor. It cannot compile this Java project by itself.

Eclipse Temurin supplies Java 17. Apache Maven gives the analyzer a repeatable build environment.

In this step, get ready to:
  • Choose an installation route that supports your macOS version.
  • Install a Java 17 toolchain with Apache Maven 3.9.16.
  • Prepare VS Code for Java development and verify both tools.
Check your macOS version

Your macOS version decides which supported installation route you use. Homebrew supports macOS Sonoma 14 or newer.

  • Select the Apple menu in the top-left corner of your screen.
  • Choose About This Mac.
  • Read the macOS version shown in the window.

✔️ I see macOS Sonoma 14 or newer

Your Mac supports the primary Homebrew installation route. You use this route in the next substep.

ⓧ I see an older macOS version

Your Mac uses the official Eclipse Temurin PKG plus the Apache Maven binary distribution. This route avoids relying on an unsupported Homebrew environment.

Install the Java toolchain

Both installation routes lead to Java 17 plus Apache Maven 3.9.16.

  • Choose the installation route that matches the macOS version you just checked.

✔️ My Mac supports Homebrew

The Homebrew installer can request your Mac password so it can place system files. Your password stays hidden while you type.

  • Install Homebrew by running this command in Terminal:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

The installer prints the changes it makes. You return to the terminal prompt when installation finishes.

Did the Homebrew installation stop?

Confirm your Mac has an internet connection. Check whether the terminal requested administrator approval.

Help me complete the Homebrew installation.

  • Install Eclipse Temurin JDK 17 by running:
brew install --cask temurin@17

The terminal prints package progress. You return to the prompt after the JDK installation completes.

  • Install Apache Maven 3.9.16 by running:
brew install maven

The terminal prints the Maven installation progress. You return to the prompt when the package is ready.

Did a package installation fail?

Check your internet connection first. Confirm that the Homebrew installation completed before retrying the package command.

Help me fix my Java or Maven installation.

  • Select Java 17 for the current terminal by running:
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH

What do these environment settings do?

  • JAVA_HOME points Java-aware tools to the installed Java 17 development kit.
  • PATH puts $JAVA_HOME/bin first for the current terminal session.

ⓧ My Mac needs the official fallback

Older macOS versions use the official vendor installation paths. The PKG handles the Java installation while the Maven guide covers its binary distribution.

  • Select Java 17 for the current terminal by running:
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH

Why select Java 17 explicitly?

JAVA_HOME identifies the JDK Maven should use. Placing its executable directory first in PATH keeps the current terminal on Java 17.

Repeat both exports whenever you create a fresh terminal.

Having trouble with the fallback installers?

Confirm that you downloaded the macOS package for Temurin. Revisit the Maven PATH section if the Maven command is unavailable.

Help me finish the manual Java setup.

Install the Java extension and verify the toolchain

The Microsoft Extension Pack for Java connects VS Code to your JDK. A fresh integrated terminal proves that the editor inherits the Java 17 environment.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code and press Enter.
  • Select Extensions in the VS Code Activity Bar.
  • Enter Extension Pack for Java in the extension search field.
  • Select the result published by Microsoft.
  • Install version 0.31.1 from the extension details page.
  • Wait until the Java language status reports ready.

The Java tools are now active inside VS Code. The final check confirms that its terminal uses the intended toolchain.

  • Create a fresh integrated terminal from the VS Code terminal menu.
  • Select Java 17 in the fresh terminal by running:
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH

Why repeat these exports?

Each fresh terminal starts with a new shell environment. These exports point that new session to Java 17 before Maven starts.

Before you check, which Java version do you expect both tools to report?

  • Verify Java plus Maven by running these commands in the fresh VS Code terminal:
java -version
mvn -v

The Java output identifies Java 17. The Maven output reports Apache Maven 3.9.16 plus Java 17.

✔️ Java 17 and Maven 3.9.16 appear

That is the toolchain ready. VS Code can now compile Java 17 projects through Apache Maven 3.9.16.

ⓧ I see a different version

Your terminal is finding another installed Java or Maven version. Start by selecting the intended JDK again.

  • Select Java 17 again by running these commands:
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH
java -version
mvn -v

Java should now identify version 17. Maven should also report that it is using Java 17.

java -version
mvn -v

Still seeing the wrong version?

A different installation may appear earlier in your shell configuration. Check that the Java exports ran in the same terminal as the version checks.

Help me correct my Java or Maven version.

ⓧ A command is not found

The missing command has not been installed or exposed through your shell environment. Use the installation route that matches your macOS version.

  • Install a missing Temurin package through Homebrew by running:
brew install --cask temurin@17
  • Install a missing Maven package through Homebrew by running:
brew install maven
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH
java -version
mvn -v

The Java output should identify Java 17. The Maven output should report Apache Maven 3.9.16.

Is a command still unavailable?

Confirm that the relevant installation completed. For the Maven archive route, revisit the documented PATH step before creating another terminal.

Help me restore the missing Java or Maven command.

Your Mac now has the Java toolchain that the analyzer needs. Next up, you build the first working sector summary.

Build the Incident Analyzer

Your Java toolchain is ready. This step turns it into a working incident analyzer that Maven can compile and run.

External files and malformed records add several failure points. You will start with three hard-coded incidents so sector classification produces a visible result first.

In this step, get ready to:
  • Create the Maven project structure with a Java 17 build configuration.
  • Model an incident with coordinates, severity, and sector classification.
  • Count three hard-coded incidents by sector and print the results.
Create the Maven project structure

A Maven project follows a predictable folder layout so the compiler can find your Java classes. Its XML configuration lives in pom.xml.

  • Switch back to the fresh VS Code terminal from earlier.
  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The cd command changes the terminal's current location. Using the Desktop gives your project a predictable home.

  • Create the project folder with its Maven source path by running this command:
mkdir -p geospatial-incident-analyzer/src/main/java/com/portfolio/geotriage

What does this command create?

The -p option creates every missing directory in the path. Your Java package files belong inside src/main/java/com/portfolio/geotriage.

  • Confirm the package directory exists by running this command:
ls geospatial-incident-analyzer/src/main/java/com/portfolio

What should you see?

You should see geotriage in the terminal output. This confirms that the complete source path exists.

  • Move the terminal into the new project folder by running this command:
cd geospatial-incident-analyzer

Why move into this folder?

Maven looks for pom.xml in the terminal's current folder. Future build commands now target this project.

Project path missing?

Check that the terminal was on your Desktop before you created the directory. A spelling difference in geospatial-incident-analyzer can also place you in the wrong folder.

Help me fix the project folder structure.

  • Select File from the VS Code menu bar.
  • Select Open Folder....
  • Select the Desktop location.
  • Select the geospatial-incident-analyzer folder.
  • Select Open.
  • Select Yes, I trust the authors if the Workspace Trust prompt appears.

You should see geospatial-incident-analyzer at the top of the Explorer. Its nested src/main/java/com/portfolio/geotriage path is ready for Java files.

  • Select the geospatial-incident-analyzer folder in the Explorer.
  • Select the New File... button in the Explorer toolbar.
  • Enter pom.xml as the file name.
  • Add the initial Maven project model by replacing the file contents with this 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.portfolio</groupId>
  <artifactId>geospatial-incident-analyzer</artifactId>
  <version>1.0.0</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
    </plugins>
  </build>
</project>

What does this project model define?

  • The coordinates identify the project as com.portfolio:geospatial-incident-analyzer at version 1.0.0.
  • The project.build.sourceEncoding property gives source files a consistent text encoding.
  • The Maven Resources Plugin version 3.5.0 gives Maven a pinned resource-processing tool.
  • Save pom.xml.
  • Compile the initial project model by running this command:
mvn compile

What does this compile prove?

Maven reads pom.xml before it looks for source code. A successful build confirms that the project model is valid.

Maven cannot read the project?

Confirm that pom.xml appears directly inside geospatial-incident-analyzer. Check the opening and closing XML tags if Maven reports a model parsing problem.

Help me troubleshoot the Maven project model.

The first compile proves Maven recognizes the project. The compiler plugin now needs an explicit Java release target.

  • In pom.xml, find the closing </plugin> tag for maven-resources-plugin.
  • Add this compiler plugin directly below that tag:
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
        <configuration>
          <release>17</release>
        </configuration>
      </plugin>

What does the compiler plugin control?

  • The Maven Compiler Plugin version 3.15.0 compiles the source files Maven finds.
  • The release value targets Java 17 language features and APIs.
  • Save pom.xml.
  • Compile with the completed plugin configuration by running this command:
mvn compile

What should happen now?

Maven should complete without compilation errors. The build now has an explicit Java 17 target.

Compiler configuration rejected?

Make sure the compiler plugin sits inside the existing plugins element. Confirm that every opening XML tag has a matching closing tag.

Help me fix the Maven compiler configuration.

✔️ Awesome, I've got everything!

Your pom.xml now defines the project coordinates, source encoding, resource plugin, and Java compiler target.

ⓧ 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.portfolio</groupId>
  <artifactId>geospatial-incident-analyzer</artifactId>
  <version>1.0.0</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
        <configuration>
          <release>17</release>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

What should you compare?

Check the nesting of the two plugin elements. Confirm that the compiler configuration contains release 17.

Model each incident

Each record needs one object that keeps its identifier, coordinates, and severity together. Private fields apply encapsulation so the class controls its own data.

  • Expand src/main/java/com/portfolio/geotriage in the Explorer.
  • Select the geotriage folder.
  • Select the New File... button.
  • Enter GeoIncident.java as the file name.
  • Add the incident type, fields, and constructor with this code:
package com.portfolio.geotriage;

public final class GeoIncident {
    public enum Severity {
        LOW,
        MEDIUM,
        HIGH
    }

    private final String id;
    private final double latitude;
    private final double longitude;
    private final Severity severity;

    public GeoIncident(String id, double latitude, double longitude, Severity severity) {
        this.id = id;
        this.latitude = latitude;
        this.longitude = longitude;
        this.severity = severity;
    }
}

What does this class define?

  • The Severity enum restricts severity to LOW, MEDIUM, or HIGH.
  • The private fields store the incident identifier, latitude, longitude, and severity.
  • The constructor requires all four values when the program creates a GeoIncident.
  • Save GeoIncident.java.
  • Compile the new class by running this command:
mvn compile

What does this check prove?

Maven should compile the new source file without errors. This confirms that the package path matches com.portfolio.geotriage.

GeoIncident does not compile?

Confirm that the filename uses the exact capitalization GeoIncident.java. Check that the file sits inside src/main/java/com/portfolio/geotriage.

Help me fix the GeoIncident compilation problem.

Sector classification uses fixed coordinate boundaries so the same coordinates always return the same result. Latitude separates north from south. Longitude separates east from west.

  • In GeoIncident.java, place your cursor before the final closing brace.
  • Add the sector classification method with this code:
    public String sector() {
        String northSouth = latitude >= 39.10 ? "NORTH" : "SOUTH";
        String eastWest = longitude >= -76.77 ? "EAST" : "WEST";
        return northSouth + "_" + eastWest;
    }

How does sector classification work?

  • A latitude of at least 39.10 produces NORTH.
  • A longitude of at least -76.77 produces EAST.
  • The method joins both labels into a value such as NORTH_EAST.
  • Save GeoIncident.java.
  • Compile the completed incident model by running this command:
mvn compile

What should happen?

Maven should compile the class without errors. The sector algorithm is now available to any code that creates a GeoIncident.

Sector method rejected?

Make sure sector() sits inside the GeoIncident class. Check the braces around the constructor before checking the method itself.

Help me fix the sector method.

✔️ Awesome, I've got everything!

Your incident model now stores one record and converts its coordinates into a sector label.

ⓧ I'd like to double check the full code

package com.portfolio.geotriage;

public final class GeoIncident {
    public enum Severity {
        LOW,
        MEDIUM,
        HIGH
    }

    private final String id;
    private final double latitude;
    private final double longitude;
    private final Severity severity;

    public GeoIncident(String id, double latitude, double longitude, Severity severity) {
        this.id = id;
        this.latitude = latitude;
        this.longitude = longitude;
        this.severity = severity;
    }

    public String sector() {
        String northSouth = latitude >= 39.10 ? "NORTH" : "SOUTH";
        String eastWest = longitude >= -76.77 ? "EAST" : "WEST";
        return northSouth + "_" + eastWest;
    }
}

What should you compare?

Check the package declaration first. Confirm that the enum, four fields, constructor, and sector() method all sit inside the class.

Aggregate sectors and run the analyzer

A Java collection lets the analyzer process several incidents as one dataset. A LinkedHashMap keeps sector totals in the order each sector first appears.

  • Select the geotriage folder in the Explorer.
  • Select the New File... button.
  • Enter IncidentAnalyzer.java as the file name.
  • Create the first runnable analyzer with this code:
package com.portfolio.geotriage;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class IncidentAnalyzer {
    private IncidentAnalyzer() {
    }

    public static void main(String[] args) {
        List<GeoIncident> incidents = List.of(
            new GeoIncident("INC-001", 39.102, -76.776, GeoIncident.Severity.HIGH),
            new GeoIncident("INC-002", 39.125, -76.800, GeoIncident.Severity.MEDIUM),
            new GeoIncident("INC-003", 39.082, -76.812, GeoIncident.Severity.LOW)
        );
        Map<String, Integer> sectorCounts = new LinkedHashMap<>();

        System.out.println("Processed incidents: " + incidents.size());
    }
}

What does this first version do?

  • The incidents list contains three synthetic records with different coordinates and severity values.
  • The sectorCounts map is ready to associate sector names with totals.
  • The first output line reports the number of records in the list.
  • Save IncidentAnalyzer.java.
  • Compile the runnable analyzer by running this command:
mvn compile

What does this compile check?

Maven now checks both Java classes together. A successful compile proves that IncidentAnalyzer can create GeoIncident objects.

Before you run the program, how many processed incidents do you expect it to report?

  • Run the first analyzer version with this command:
java -cp target/classes com.portfolio.geotriage.IncidentAnalyzer

What should you see?

You should see Processed incidents: 3. The Java launcher found the compiled class under target/classes.

That is your first working result. The analyzer can now create and process its complete hard-coded dataset.

Analyzer class not found?

Confirm that the compile command completed before you ran the class. Check that IncidentAnalyzer.java declares the package com.portfolio.geotriage.

Help me run the compiled analyzer.

The processed total proves the records exist. The final version also asks each incident for its sector before updating the map.

  • In IncidentAnalyzer.java, find this output line:
        System.out.println("Processed incidents: " + incidents.size());

What does the current line report?

This line reports only the list size. It does not print the sector counts stored in sectorCounts.

  • Replace that output line with this aggregation and reporting code:
        for (GeoIncident incident : incidents) {
            String sector = incident.sector();
            sectorCounts.put(sector, sectorCounts.getOrDefault(sector, 0) + 1);
        }

        System.out.println("Processed incidents: " + incidents.size());
        for (Map.Entry<String, Integer> entry : sectorCounts.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

How does aggregation work?

  • The first loop calls sector() for each incident.
  • The getOrDefault() call starts an unseen sector at zero before adding one.
  • The second loop prints each map entry in insertion order.
  • Save IncidentAnalyzer.java.

✔️ Awesome, I've got everything!

Your analyzer now creates three incidents, classifies each one, counts its sector, and prints the totals.

ⓧ I'd like to double check the full code

package com.portfolio.geotriage;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class IncidentAnalyzer {
    private IncidentAnalyzer() {
    }

    public static void main(String[] args) {
        List<GeoIncident> incidents = List.of(
            new GeoIncident("INC-001", 39.102, -76.776, GeoIncident.Severity.HIGH),
            new GeoIncident("INC-002", 39.125, -76.800, GeoIncident.Severity.MEDIUM),
            new GeoIncident("INC-003", 39.082, -76.812, GeoIncident.Severity.LOW)
        );
        Map<String, Integer> sectorCounts = new LinkedHashMap<>();

        for (GeoIncident incident : incidents) {
            String sector = incident.sector();
            sectorCounts.put(sector, sectorCounts.getOrDefault(sector, 0) + 1);
        }

        System.out.println("Processed incidents: " + incidents.size());
        for (Map.Entry<String, Integer> entry : sectorCounts.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

What should you compare?

Confirm that the incident list appears before the empty map. Check that aggregation happens before either output loop.

Before the final run, which sector do you expect to have the largest count from these three coordinate pairs?

  • Compile the completed analyzer by running this command:
mvn compile

What should the build confirm?

Maven should compile both source files without errors. Your saved code is ready for the final runtime check.

  • Run the completed analyzer with this command:
java -cp target/classes com.portfolio.geotriage.IncidentAnalyzer

What should you see?

You should see Processed incidents: 3. The sector totals should include NORTH_WEST: 2 and SOUTH_WEST: 1.

The output follows the coordinate comparisons in sector(). Both northern longitudes are below -76.77, so they classify as west.

Sector totals look different?

Compare all three coordinates with the hard-coded incident list. Check the signs on the longitude values because a missing minus sign changes the east-west result.

Help me diagnose the incident totals.

Your analyzer now turns hard-coded coordinates into a repeatable sector summary. Next, you will replace those in-memory records with CSV data and observe how malformed input affects the program.

Load CSV Data and Inspect the Failure

Your hard-coded Java analyzer already turns three incidents into sector totals. Real incident feeds arrive as text files with values your code does not control.

This step moves the incident records into a CSV file. You will let malformed data stop the run so you can inspect the weakness in the current pipeline.

In this step, get ready to:
  • Create a six-record CSV dataset containing valid records and malformed values.
  • Add CSV row parsing to GeoIncident.
  • Run the file-backed analyzer to inspect its planned failure.
Create the CSV dataset

A CSV file stores each incident on one line. Its columns give the analyzer a consistent order for the incident ID, coordinates, and severity.

  • Select the geospatial-incident-analyzer folder in the Explorer sidebar.
  • Use the folder icon with a plus sign at the top of the Explorer sidebar.
  • Type data as the folder name.
  • Press Enter.

You should see a data folder inside geospatial-incident-analyzer.

  • Select the data folder in the Explorer sidebar.
  • Use the file icon with a plus sign at the top of the Explorer sidebar.
  • Type incidents.csv as the file name.
  • Press Enter.
  • Fill data/incidents.csv with six incidents by pasting this dataset:
id,latitude,longitude,severity
INC-001,39.102,-76.776,HIGH
INC-002,39.125,-76.744,MEDIUM
INC-003,39.082,-76.812,LOW
INC-004,not-a-number,-76.700,HIGH
INC-005,39.097,-76.750,CRITICAL
INC-006,39.110,-76.755,HIGH

What is in this dataset?

  • The header defines the four values expected in every incident record.
  • The six synthetic rows provide repeatable input for the analyzer.
  • The not-a-number value tests coordinate conversion.
  • The CRITICAL value tests conversion into the supported severity values.
  • Save data/incidents.csv.

You should see one header followed by six data rows. The malformed coordinate appears on CSV line 5.

Is the dataset in the wrong place?

  • Confirm that incidents.csv appears inside the data folder.
  • Check that the data folder appears directly inside geospatial-incident-analyzer.

Help me fix the CSV file location.

✔️ Awesome, I've got everything!

Your saved data/incidents.csv file contains the header and all six incident records.

ⓧ I'd like to double check the full code

Compare your complete data/incidents.csv file with this reference.

id,latitude,longitude,severity
INC-001,39.102,-76.776,HIGH
INC-002,39.125,-76.744,MEDIUM
INC-003,39.082,-76.812,LOW
INC-004,not-a-number,-76.700,HIGH
INC-005,39.097,-76.750,CRITICAL
INC-006,39.110,-76.755,HIGH

What should match?

The record order matters because the analyzer reads from top to bottom. The malformed coordinate must remain on CSV line 5 for the planned failure.

Parse each CSV row

The fromCsv(String) factory method turns one text row into a GeoIncident. Validation inside this method keeps the conversion rules close to the object they create.

  • Switch back to GeoIncident.java in the editor.
  • Place this incident ID check at the start of the constructor:
        if (id == null || id.isBlank()) {
            throw new IllegalArgumentException("Incident ID is required");
        }

Why validate the incident ID?

Every accepted incident needs an identifier. The constructor throws an IllegalArgumentException when the ID is missing or blank.

  • Add fromCsv(String) directly above sector() by pasting this method:
    public static GeoIncident fromCsv(String line) {
        String[] columns = line.split(",", -1);
        if (columns.length != 4) {
            throw new IllegalArgumentException("Expected 4 columns");
        }

        String id = columns[0].trim();
        double latitude;
        double longitude;

        try {
            latitude = Double.parseDouble(columns[1].trim());
            longitude = Double.parseDouble(columns[2].trim());
        } catch (NumberFormatException exception) {
            throw new IllegalArgumentException("Coordinates must be numbers");
        }

        Severity severity;
        try {
            severity = Severity.valueOf(columns[3].trim());
        } catch (IllegalArgumentException exception) {
            throw new IllegalArgumentException("Severity must be LOW, MEDIUM, or HIGH");
        }

        return new GeoIncident(id, latitude, longitude, severity);
    }

What does this parser do?

  • The column count check rejects rows that do not contain four values.
  • Double.parseDouble(String) converts the coordinate text into numbers.
  • The numeric conversion catch replaces a low-level parsing failure with the project message Coordinates must be numbers.
  • Severity.valueOf(String) converts the final column into a supported Severity value.
  • Save GeoIncident.java.

✔️ Awesome, I've got everything!

Your GeoIncident.java file now validates IDs and converts CSV rows into incidents.

ⓧ I'd like to double check the full code

Compare your complete GeoIncident.java file with this reference.

package com.portfolio.geotriage;

public final class GeoIncident {
    public enum Severity {
        LOW,
        MEDIUM,
        HIGH
    }

    private final String id;
    private final double latitude;
    private final double longitude;
    private final Severity severity;

    public GeoIncident(String id, double latitude, double longitude, Severity severity) {
        if (id == null || id.isBlank()) {
            throw new IllegalArgumentException("Incident ID is required");
        }
        this.id = id;
        this.latitude = latitude;
        this.longitude = longitude;
        this.severity = severity;
    }

    public static GeoIncident fromCsv(String line) {
        String[] columns = line.split(",", -1);
        if (columns.length != 4) {
            throw new IllegalArgumentException("Expected 4 columns");
        }

        String id = columns[0].trim();
        double latitude;
        double longitude;

        try {
            latitude = Double.parseDouble(columns[1].trim());
            longitude = Double.parseDouble(columns[2].trim());
        } catch (NumberFormatException exception) {
            throw new IllegalArgumentException("Coordinates must be numbers");
        }

        Severity severity;
        try {
            severity = Severity.valueOf(columns[3].trim());
        } catch (IllegalArgumentException exception) {
            throw new IllegalArgumentException("Severity must be LOW, MEDIUM, or HIGH");
        }

        return new GeoIncident(id, latitude, longitude, severity);
    }

    public String sector() {
        String northSouth = latitude >= 39.10 ? "NORTH" : "SOUTH";
        String eastWest = longitude >= -76.77 ? "EAST" : "WEST";
        return northSouth + "_" + eastWest;
    }
}

What should match?

The constructor guard appears before the field assignments. The fromCsv(String) method appears before sector().

  • Switch back to the VS Code terminal.
  • Compile the updated parser by running this command:
mvn compile

Maven should finish successfully with no compilation errors. This confirms that the new factory method is valid Java code.

Does the parser fail to compile?

  • Check that fromCsv(String) sits inside the GeoIncident class.
  • Compare the braces around both try blocks with the full-file reference.
  • Confirm that each statement inside the method ends with the required punctuation.

Help me fix the GeoIncident compilation problem.

Read the file and inspect the failure

The analyzer can now replace its hard-coded objects with rows from data/incidents.csv. It starts at index 1 so the CSV header stays out of the incident count.

  • Switch back to IncidentAnalyzer.java in the editor.
  • Replace the entire file with this file-reading version:
package com.portfolio.geotriage;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class IncidentAnalyzer {
    private IncidentAnalyzer() {
    }

    public static void main(String[] args) throws IOException {
        List<String> lines = Files.readAllLines(Path.of("data", "incidents.csv"));
        Map<String, Integer> sectorCounts = new LinkedHashMap<>();

        for (int index = 1; index < lines.size(); index++) {
            GeoIncident incident = GeoIncident.fromCsv(lines.get(index));
            String sector = incident.sector();
            sectorCounts.put(sector, sectorCounts.getOrDefault(sector, 0) + 1);
        }

        for (Map.Entry<String, Integer> entry : sectorCounts.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

How does file processing work?

  • Files.readAllLines(Path) loads the dataset into a List<String>.
  • The loop begins at index 1 to skip the header row.
  • GeoIncident.fromCsv(String) converts each remaining row into an incident.
  • The existing LinkedHashMap logic counts each accepted incident by sector.
  • Save IncidentAnalyzer.java.

✔️ Awesome, I've got everything!

Your saved analyzer now reads incident rows from data/incidents.csv.

ⓧ I'd like to double check the full code

Compare your complete IncidentAnalyzer.java file with this reference.

package com.portfolio.geotriage;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class IncidentAnalyzer {
    private IncidentAnalyzer() {
    }

    public static void main(String[] args) throws IOException {
        List<String> lines = Files.readAllLines(Path.of("data", "incidents.csv"));
        Map<String, Integer> sectorCounts = new LinkedHashMap<>();

        for (int index = 1; index < lines.size(); index++) {
            GeoIncident incident = GeoIncident.fromCsv(lines.get(index));
            String sector = incident.sector();
            sectorCounts.put(sector, sectorCounts.getOrDefault(sector, 0) + 1);
        }

        for (Map.Entry<String, Integer> entry : sectorCounts.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

What should match?

The imports include the Java file APIs. The hard-coded List<GeoIncident> has been replaced by the lines loaded from the CSV file.

  • Switch back to the VS Code terminal.
  • Compile the file-backed analyzer by running this command:
mvn compile

Good progress. Maven now accepts the CSV parser and the file-backed analyzer.

Does the analyzer fail to compile?

  • Confirm that the file API imports appear above the collection imports.
  • Check that main(String[]) includes throws IOException.
  • Compare the loop boundaries with the full-file reference.

Help me fix the IncidentAnalyzer compilation problem.

The next run is an intentional failure check. The current analyzer sends every data row directly into the parser.

Before you run it, predict whether the analyzer reaches the end of all six records.

  • Run the file-backed analyzer with this command:
java -cp target/classes com.portfolio.geotriage.IncidentAnalyzer

You will see the analyzer stop when it reaches CSV line 5. The terminal reports an IllegalArgumentException stating Coordinates must be numbers.

Why does the report stop?

The fromCsv(String) method detects that not-a-number cannot become a coordinate. It throws the project-specific exception from the numeric conversion catch.

The loop has no per-record catch boundary. The exception therefore leaves main(String[]) before the sector totals can print.

This failure is the intended result for this step. The later CRITICAL value never reaches severity conversion.

You have exposed the exact point where one bad record defeats the full report. Next, you will contain record-level failures so every valid incident still reaches the summary.

Make the Pipeline Resilient

Your Maven project now reads realistic incident data. One malformed coordinate currently stops the entire report.

Java record-level exception handling lets each invalid row fail safely. A dedicated result object then carries the complete analysis into the report.

In this step, get ready to:
  • Create deterministic sector state for every analysis.
  • Catch invalid records without stopping the remaining rows.
  • Print the final totals with the highest-risk sector.
Create deterministic result state

A report needs a stable order even when a sector has no incidents. The SECTORS array defines that order before any records are processed.

  • Select IncidentAnalyzer.java in the VS Code Explorer sidebar.
  • Find the IncidentAnalyzer class declaration.
  • Add the sector order directly below the class declaration by copying this code:
    private static final String[] SECTORS = {
        "NORTH_EAST",
        "NORTH_WEST",
        "SOUTH_EAST",
        "SOUTH_WEST"
    };

How Does the Sector Order Help?

  • The array lists all four project-defined sectors.
  • Its fixed order keeps the final report predictable.
  • Including every sector allows zero-count sectors to appear in the summary.
  • Save IncidentAnalyzer.java.
  • Confirm the sector definition compiles by running this command in the VS Code terminal:
mvn compile

What Does This Check Prove?

Maven recompiles the project with the new sector array. The build finishes successfully when the array is inside the IncidentAnalyzer class.

Does the Compilation Fail?

Check that the sector array sits inside the outer class. Make sure the closing brace and comma placement match the snippet.

Help me fix the sector array compilation problem.

The calculations also need one object that carries processed counts through the program. This encapsulation keeps analysis state together.

  • Scroll to the final closing brace in IncidentAnalyzer.java.
  • Start the nested AnalysisResult class immediately before that final brace by copying this first section:
    public static final class AnalysisResult {
        private final int processed;
        private final int skipped;
        private final Map<String, Integer> sectorCounts;

        public AnalysisResult(int processed, int skipped, Map<String, Integer> sectorCounts) {
            this.processed = processed;
            this.skipped = skipped;
            this.sectorCounts = sectorCounts;
        }

        public int processed() {
            return processed;
        }

        public int skipped() {
            return skipped;
        }

        public Map<String, Integer> sectorCounts() {
            return sectorCounts;
        }

What Does the Result Store?

  • The private fields hold successful records, rejected records, and sector totals.
  • The constructor receives one complete analysis state.
  • The accessor methods expose each value to the reporting code.
  • Complete AnalysisResult directly below the sectorCounts() method by copying this section:
        public String highestRiskSector() {
            String highest = SECTORS[0];
            for (String sector : SECTORS) {
                if (sectorCounts.get(sector) > sectorCounts.get(highest)) {
                    highest = sector;
                }
            }
            return highest;
        }
    }

How Is the Highest-Risk Sector Chosen?

The method starts with the first sector as its current candidate. It replaces that candidate only when it finds a larger count.

Equal counts keep the earlier sector from SECTORS. This makes the selection deterministic.

  • Save IncidentAnalyzer.java.
  • Confirm the completed result class compiles by running:
mvn compile

What Should Happen?

The project compiles with the new nested result class. This confirms its fields and methods can use the outer class's sector order.

Is the Result Class Rejected?

Check that AnalysisResult appears before the outer class's final brace. Compare each method brace with the two code sections.

Help me repair the AnalysisResult class.

Contain failures for each record

The analyzer needs a separate failure boundary around each CSV row. A rejected row increments skipped while the loop continues to the next record.

  • Find the closing brace of the current main() method in IncidentAnalyzer.java.
  • Add the analyze() method directly below main() by copying this code:
    public static AnalysisResult analyze(Path csvPath) throws IOException {
        List<String> lines = Files.readAllLines(csvPath);
        Map<String, Integer> sectorCounts = new LinkedHashMap<>();

        for (String sector : SECTORS) {
            sectorCounts.put(sector, 0);
        }

        int processed = 0;
        int skipped = 0;

        for (int index = 1; index < lines.size(); index++) {
            String line = lines.get(index);
            if (line.isBlank()) {
                continue;
            }

            try {
                GeoIncident incident = GeoIncident.fromCsv(line);
                String sector = incident.sector();
                sectorCounts.put(sector, sectorCounts.get(sector) + 1);
                processed++;
            } catch (IllegalArgumentException exception) {
                skipped++;
                System.out.println("Skipped line " + (index + 1));
            }
        }

        return new AnalysisResult(processed, skipped, sectorCounts);
    }

What Does the Analysis Method Do?

  • The initial loop gives every sector a starting count of zero.
  • The row loop ignores the CSV header by starting at index one.
  • The try block parses and counts one valid incident.
  • The catch block records one rejected row without ending the loop.
  • Save IncidentAnalyzer.java.
  • Verify the record-level analysis compiles by running:
mvn compile

What Does Compilation Confirm?

The successful build confirms that analyze() can create incidents, count sectors, catch rejected rows, and return AnalysisResult.

Does the New Method Fail to Compile?

Check that analyze() sits outside main(). Confirm that it remains inside the outer IncidentAnalyzer class.

Help me fix the analyze method.

Print a stable operational summary

The analysis now produces a complete result. The reporting method turns that result into stable sector totals plus one highest-risk sector.

  • Find the closing brace of analyze() in IncidentAnalyzer.java.
  • Add printSummary() directly below analyze() by copying this code:
    private static void printSummary(AnalysisResult result) {
        System.out.println("Processed incidents: " + result.processed());
        System.out.println("Skipped records: " + result.skipped());
        System.out.println("Sector totals:");

        for (String sector : SECTORS) {
            System.out.println(sector + ": " + result.sectorCounts().get(sector));
        }

        String highest = result.highestRiskSector();
        System.out.println(
            "Highest-risk sector: " + highest + " (" + result.sectorCounts().get(highest) + ")"
        );
    }

How Is the Report Structured?

  • The first lines show accepted and rejected record totals.
  • The sector loop follows the fixed SECTORS order.
  • The final line combines the highest-risk sector with its count.
  • Save IncidentAnalyzer.java.
  • Confirm the reporting method compiles by running:
mvn compile

What Does This Build Check?

The successful compilation confirms that the report can read every value exposed by AnalysisResult.

The final connection belongs in main(). It sends the CSV path into analyze() before passing the returned result into printSummary().

  • Find the current main() method near the top of IncidentAnalyzer.java.
  • Replace the complete main() method with this version:
    public static void main(String[] args) throws IOException {
        AnalysisResult result = analyze(Path.of("data", "incidents.csv"));
        printSummary(result);
    }

What Changed in Main?

The entry point now delegates calculations to analyze(). It delegates terminal formatting to printSummary().

These method boundaries make each responsibility easier to verify independently.

  • Save IncidentAnalyzer.java.

✔️ Awesome, I've got everything!

Your resilient analyzer is assembled. Double check that IncidentAnalyzer.java is saved before compiling it.

ⓧ I'd like to double check the full code

Compare IncidentAnalyzer.java with this complete version.

package com.portfolio.geotriage;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class IncidentAnalyzer {
    private static final String[] SECTORS = {
        "NORTH_EAST",
        "NORTH_WEST",
        "SOUTH_EAST",
        "SOUTH_WEST"
    };

    private IncidentAnalyzer() {
    }

    public static void main(String[] args) throws IOException {
        AnalysisResult result = analyze(Path.of("data", "incidents.csv"));
        printSummary(result);
    }

    public static AnalysisResult analyze(Path csvPath) throws IOException {
        List<String> lines = Files.readAllLines(csvPath);
        Map<String, Integer> sectorCounts = new LinkedHashMap<>();

        for (String sector : SECTORS) {
            sectorCounts.put(sector, 0);
        }

        int processed = 0;
        int skipped = 0;

        for (int index = 1; index < lines.size(); index++) {
            String line = lines.get(index);
            if (line.isBlank()) {
                continue;
            }

            try {
                GeoIncident incident = GeoIncident.fromCsv(line);
                String sector = incident.sector();
                sectorCounts.put(sector, sectorCounts.get(sector) + 1);
                processed++;
            } catch (IllegalArgumentException exception) {
                skipped++;
                System.out.println("Skipped line " + (index + 1));
            }
        }

        return new AnalysisResult(processed, skipped, sectorCounts);
    }

    private static void printSummary(AnalysisResult result) {
        System.out.println("Processed incidents: " + result.processed());
        System.out.println("Skipped records: " + result.skipped());
        System.out.println("Sector totals:");

        for (String sector : SECTORS) {
            System.out.println(sector + ": " + result.sectorCounts().get(sector));
        }

        String highest = result.highestRiskSector();
        System.out.println(
            "Highest-risk sector: " + highest + " (" + result.sectorCounts().get(highest) + ")"
        );
    }

    public static final class AnalysisResult {
        private final int processed;
        private final int skipped;
        private final Map<String, Integer> sectorCounts;

        public AnalysisResult(int processed, int skipped, Map<String, Integer> sectorCounts) {
            this.processed = processed;
            this.skipped = skipped;
            this.sectorCounts = sectorCounts;
        }

        public int processed() {
            return processed;
        }

        public int skipped() {
            return skipped;
        }

        public Map<String, Integer> sectorCounts() {
            return sectorCounts;
        }

        public String highestRiskSector() {
            String highest = SECTORS[0];
            for (String sector : SECTORS) {
                if (sectorCounts.get(sector) > sectorCounts.get(highest)) {
                    highest = sector;
                }
            }
            return highest;
        }
    }
}
  • Compile the completed analyzer by running:
mvn compile

What Does a Successful Compile Prove?

The build confirms that the parser, record-level recovery, result object, and reporting methods connect correctly.

Before you run the analyzer, predict whether both invalid rows can be skipped while the remaining incidents still produce a complete report.

  • Run the resilient analyzer with this command:
java -cp target/classes com.portfolio.geotriage.IncidentAnalyzer

What Should You See?

  • You should see Skipped line 5 for the malformed coordinate.
  • You should see Skipped line 6 for the unsupported severity.
  • The report should show Processed incidents: 4 plus Skipped records: 2.
  • The sector totals should show NORTH_EAST: 2, NORTH_WEST: 0, SOUTH_EAST: 1, and SOUTH_WEST: 1.
  • The final line should show Highest-risk sector: NORTH_EAST (2).

You have repaired the pipeline. Invalid records are now isolated while every valid incident reaches the operational summary.

Does the Analyzer Still Stop Early?

Check that GeoIncident.fromCsv(line) sits inside the per-record try block. Confirm that the catch block closes before the row loop ends.

If a sector count differs, compare data/incidents.csv with the six-record dataset from the previous step.

Help me troubleshoot the resilient analyzer output.

Your analyzer now produces a complete report from imperfect input. Next, you will turn these requirements into repeatable automated verification checks.

Add Automated Verification

Your resilient analyzer now finishes its report even when the dataset contains bad records. That result gives you a reliable baseline for automated checks.

Manual checks are easy to skip after a code change. In this step, you will build a repeatable Java verification suite that uses your existing Maven project.

In this step, get ready to:
  • Create assertion helpers that stop the suite when a requirement fails.
  • Verify classification plus malformed coordinate and unsupported severity handling.
  • Check the complete analysis result before running both programs.
Build the verification helpers

Each verification needs a clear pass-or-fail rule. The helper methods centralize that rule so every failed requirement produces an AssertionError.

  • Select the src/main/java/com/portfolio/geotriage folder in the VS Code file sidebar.
  • Use the new-file control beside the folder name.
  • Name the file VerificationSuite.java.
  • Create the verification helper foundation by adding this code:
package com.portfolio.geotriage;

import java.io.IOException;
import java.nio.file.Path;

public final class VerificationSuite {
    private VerificationSuite() {
    }

    private static void expectIllegalArgument(Runnable action, String message) {
        try {
            action.run();
        } catch (IllegalArgumentException exception) {
            return;
        }
        throw new AssertionError(message);
    }

    private static void check(boolean condition, String message) {
        if (!condition) {
            throw new AssertionError(message);
        }
    }
}

What do these helpers do?

  • The check() helper accepts a condition that represents one requirement.
  • A false condition throws an AssertionError with a message that identifies the failed requirement.
  • The expectIllegalArgument() helper passes only when the supplied action throws the expected exception.
  • Save VerificationSuite.java.
  • Confirm the helper foundation compiles by running this command in the VS Code terminal:
mvn compile

What does this command prove?

Maven compiles the new class with the rest of your project. A completed build confirms that both helper signatures are valid Java.

Seeing a compilation error?

Confirm that VerificationSuite.java sits beside GeoIncident.java inside the geotriage folder. Check the reported line for a missing brace or parenthesis.

Ask for help with the exact compiler output: Help me fix the verification helper compilation error

Add focused behavior checks

Focused checks isolate one behavior at a time. You will cover successful classification before testing the two invalid input paths.

  • Insert the classification check directly above expectIllegalArgument() by adding this method:
    private static void checksNorthEastClassification() {
        GeoIncident incident = GeoIncident.fromCsv("TEST-001,39.110,-76.755,HIGH");
        check("NORTH_EAST".equals(incident.sector()), "Expected NORTH_EAST sector");
    }

What does this check prove?

The method parses one valid incident through GeoIncident.fromCsv(). It then verifies that the coordinate split produces NORTH_EAST.

  • Save VerificationSuite.java.
  • Confirm the classification check compiles by running:
mvn compile

What does this compilation confirm?

The completed build confirms that the check can call your existing parsing and sector methods. The test data now follows the same path as a real CSV record.

Classification check not compiling?

Check that the method is inside the VerificationSuite class. Confirm that its closing brace appears before expectIllegalArgument().

Ask for help with the compiler output: Help me fix the classification check

  • Insert the malformed coordinate check directly below checksNorthEastClassification() by adding this method:
    private static void rejectsMalformedCoordinate() {
        expectIllegalArgument(
            () -> GeoIncident.fromCsv("TEST-002,not-a-number,-76.755,HIGH"),
            "Expected malformed coordinate to be rejected"
        );
    }

How does this test rejection?

The lambda sends not-a-number through the real parser. The helper treats the resulting IllegalArgumentException as a successful verification.

  • Save VerificationSuite.java.
  • Confirm the malformed coordinate check compiles by running:
mvn compile

What does this build establish?

The completed build confirms that the lambda matches the helper's Runnable parameter. Your suite can now represent an operation that should fail safely.

Seeing an error around the lambda?

Compare the parentheses around expectIllegalArgument() with the code above. Confirm that the lambda begins with () ->.

Ask for help with the reported line: Help me fix the malformed coordinate check

  • Insert the unsupported severity check directly below rejectsMalformedCoordinate() by adding this method:
    private static void rejectsUnknownSeverity() {
        expectIllegalArgument(
            () -> GeoIncident.fromCsv("TEST-003,39.110,-76.755,CRITICAL"),
            "Expected unknown severity to be rejected"
        );
    }

What requirement does this cover?

The input uses CRITICAL outside the supported severity enum. The check proves that the parser rejects the value with an IllegalArgumentException.

  • Save VerificationSuite.java.
  • Confirm all three focused checks compile by running:
mvn compile

What have you verified so far?

The suite now contains one successful classification case. It also contains separate malformed coordinate and unsupported severity cases.

Unknown severity check not compiling?

Confirm that rejectsUnknownSeverity() appears inside the class. Check that each method has its own opening and closing braces.

Ask for help with the method structure: Help me fix the unsupported severity check

Verify the complete analysis

The focused methods validate individual parsing rules. The final analysis check proves that those rules still produce the required result across the complete dataset.

  • Insert the complete analysis check directly below rejectsUnknownSeverity() by adding this method:
    private static void checksCompleteAnalysis() throws IOException {
        IncidentAnalyzer.AnalysisResult result =
            IncidentAnalyzer.analyze(Path.of("data", "incidents.csv"));

        check(result.processed() == 4, "Expected 4 processed incidents");
        check(result.skipped() == 2, "Expected 2 skipped records");
        check(result.sectorCounts().get("NORTH_EAST") == 2, "Expected 2 NORTH_EAST incidents");
        check("NORTH_EAST".equals(result.highestRiskSector()), "Expected NORTH_EAST to rank highest");
    }

What does the complete check cover?

  • The method runs IncidentAnalyzer.analyze() against the six-record dataset.
  • The first two assertions verify that four records are processed and two are skipped.
  • The final assertions verify a NORTH_EAST count of 2 plus the highest-risk result.
  • Save VerificationSuite.java.
  • Confirm the complete analysis check compiles by running:
mvn compile

What does this compilation connect?

The suite now reads data/incidents.csv through the public analysis method. This connects the parser and aggregation result in one repeatable check.

Complete analysis check not compiling?

Confirm that the file still imports java.io.IOException and java.nio.file.Path near the top. Check that the method includes throws IOException.

Ask for help with the integration check: Help me fix the complete analysis check

  • Insert the suite entry point directly below the private constructor by adding this method:
    public static void main(String[] args) throws IOException {
        checksNorthEastClassification();
        rejectsMalformedCoordinate();
        rejectsUnknownSeverity();
        checksCompleteAnalysis();
        System.out.println("All verification checks passed.");
    }

How does the suite run?

The main() method runs each check in a fixed order. Any failed assertion stops execution before the final confirmation message.

  • Save VerificationSuite.java.
  • Confirm the complete suite compiles by running:
mvn compile

What does the final compilation prove?

Maven now compiles the entry point plus every helper and check. The suite is ready to run as a Java program.

Main method not compiling?

Compare every method name in main() with its declaration below. Java method names are case-sensitive.

Ask for help with the reported symbol: Help me fix the verification suite entry point

✔️ Awesome, I've got everything!

Your saved VerificationSuite.java now contains four checks plus two reusable helpers.

ⓧ I'd like to double check the full code

Compare your complete VerificationSuite.java with this reference:

package com.portfolio.geotriage;

import java.io.IOException;
import java.nio.file.Path;

public final class VerificationSuite {
    private VerificationSuite() {
    }

    public static void main(String[] args) throws IOException {
        checksNorthEastClassification();
        rejectsMalformedCoordinate();
        rejectsUnknownSeverity();
        checksCompleteAnalysis();
        System.out.println("All verification checks passed.");
    }

    private static void checksNorthEastClassification() {
        GeoIncident incident = GeoIncident.fromCsv("TEST-001,39.110,-76.755,HIGH");
        check("NORTH_EAST".equals(incident.sector()), "Expected NORTH_EAST sector");
    }

    private static void rejectsMalformedCoordinate() {
        expectIllegalArgument(
            () -> GeoIncident.fromCsv("TEST-002,not-a-number,-76.755,HIGH"),
            "Expected malformed coordinate to be rejected"
        );
    }

    private static void rejectsUnknownSeverity() {
        expectIllegalArgument(
            () -> GeoIncident.fromCsv("TEST-003,39.110,-76.755,CRITICAL"),
            "Expected unknown severity to be rejected"
        );
    }

    private static void checksCompleteAnalysis() throws IOException {
        IncidentAnalyzer.AnalysisResult result =
            IncidentAnalyzer.analyze(Path.of("data", "incidents.csv"));

        check(result.processed() == 4, "Expected 4 processed incidents");
        check(result.skipped() == 2, "Expected 2 skipped records");
        check(result.sectorCounts().get("NORTH_EAST") == 2, "Expected 2 NORTH_EAST incidents");
        check("NORTH_EAST".equals(result.highestRiskSector()), "Expected NORTH_EAST to rank highest");
    }

    private static void expectIllegalArgument(Runnable action, String message) {
        try {
            action.run();
        } catch (IllegalArgumentException exception) {
            return;
        }
        throw new AssertionError(message);
    }

    private static void check(boolean condition, String message) {
        if (!condition) {
            throw new AssertionError(message);
        }
    }
}

How to use this reference

Check the method order plus every brace and string value. Your saved file should match this reference exactly.

The analyzer and verification suite now share the same production methods. The final run proves that the report still works before the automated checks evaluate its requirements.

  • Run the analyzer from the VS Code terminal with this command:
java -cp target/classes com.portfolio.geotriage.IncidentAnalyzer

What should the analyzer report?

You should see two skipped line messages followed by Processed incidents: 4 and Skipped records: 2.

The sector totals should end with Highest-risk sector: NORTH_EAST (2).

Analyzer output changed?

Confirm that data/incidents.csv still contains the original six records. Check that you ran the command from the geospatial-incident-analyzer folder.

Ask for help comparing the report: Help me diagnose the changed analyzer output

Before you run the suite, do you expect it to stop on a failed requirement or reach its final confirmation?

  • Run the automated verification suite with this command:
java -cp target/classes com.portfolio.geotriage.VerificationSuite

What should the suite confirm?

You should see the skipped line messages when the complete analysis check processes the dataset. The final line should read All verification checks passed.

That final message appears only after classification and rejection behavior pass. It also confirms the processed totals plus the highest-risk result.

Verification suite stopped early?

Read the AssertionError message to identify the failed requirement. Compare that check with the current output from IncidentAnalyzer.

Ask for help with the failed requirement: Help me debug the failed verification check

Where can this design go next?

A future Spring Boot controller could call analyze(Path) and return the result through an API. A future Hadoop mapper could reuse the parsing and sector classification boundaries for distributed records.

You did it. Your analyzer now has a repeatable safety net for every core requirement.

Secret mission

Add Severity-Level Triage

Your sector report shows where accepted incidents happened. Extend it with deterministic LOW, MEDIUM, and HIGH totals so an analyst can also see the urgency across those incidents.

Clean Up Your Resources

Clean Up Your Resources

Your local analyzer creates no ongoing cloud costs. Choose whether to keep the project available, pause your work, or remove its local resources.

Resources you used:

  • Local Maven project folder geospatial-incident-analyzer, including its source code, synthetic CSV data, and compiled target output.
  • Apache Maven 3.9.16 installed through Homebrew.
  • Eclipse Temurin Java Development Kit 17 installed through Homebrew.

Keep everything running

Leave your current setup in place if you want to keep developing the analyzer. The project does not run a background service while its commands are idle.

  • Keep the geospatial-incident-analyzer folder for future Java projects.
  • Reuse Maven with Eclipse Temurin JDK 17 for later builds.
  • Continue using the Extension Pack for Java in Visual Studio Code.

Pause - I'll come back to this later

Your project has no background service to shut down. Closing Visual Studio Code pauses your work while preserving every file and tool.

  • Close Visual Studio Code.
  • Reopen the existing geospatial-incident-analyzer folder when you return.
  • Select Java 17 in the fresh VS Code terminal by running:
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH

What Do These Commands Do?

  • The JAVA_HOME variable points the current terminal at JDK 17.
  • The PATH variable makes that Java installation available to terminal commands.
  • Confirm the restored toolchain by running:
java -version
mvn -v

What Should I See?

You should see Java 17 in the first result. You should see Apache Maven 3.9.16 in the second result.

Seeing a Different Java Version?

  • Rerun the two export commands above if either result uses a different Java version.
  • Help me restore the expected Java version in my VS Code terminal.

Delete - I don't want to use this again

Deleting the project folder becomes permanent when you empty Trash. Keep a copy if you want to revisit the analyzer.

Removing Maven or Temurin affects other Java projects. Keep those tools if you plan to reuse them.

  • Close Visual Studio Code.
  • Use macOS search to locate the geospatial-incident-analyzer folder.
  • Move the geospatial-incident-analyzer folder to Trash.
  • Review the contents of Trash.
  • Empty Trash when you are ready to delete its contents permanently.
  • Remove Maven and Temurin by running these commands if you do not need the tools elsewhere:
brew uninstall maven
brew uninstall --cask temurin@17

What Do These Commands Remove?

  • The first command removes the Maven formula installed through Homebrew.
  • The second command removes the Temurin 17 cask installed through Homebrew.
  • Review the terminal output to confirm both tools were removed.

Homebrew Cannot Remove a Tool?

  • Check whether Maven or Temurin was installed through another method.
  • Help me identify how my Java tools were installed and remove them safely.

Nice Work!

Nice Work!

You made it! Your resilient Java incident analyzer now converts synthetic CSV data into a verified operational report while malformed records are safely skipped.

You've learned how to:

  • Model each geospatial incident with an encapsulated Java class. Use a deterministic coordinate algorithm to assign one of four geographic sectors.
  • Trace a planned malformed-data failure to the offending CSV row. Repair the pipeline with record-level exception handling so valid incidents still reach the report.
  • Compile the project with Maven. Run an automated verification suite covering classification, rejected records, final totals, plus highest-risk sector selection.
  • Secret Mission: Add severity-level triage with verified totals for LOW, MEDIUM, plus HIGH incidents.

Ready to quiz yourself?