Week 8: GUI, File I/O and MVC

MVC, file I/O from a GIS angle, images as pixel grids, exception handling · 08.06.2026

Slides

Download Week 8 Slides (PDF) →

Code Examples

Code/Week8/src/at/ac/univie/gis/week8/
├── mvc/                       the MVC pattern, before and after
│   ├── before/MonolithicCanvas.java   one JPanel does everything (the problem)
│   └── after/                          the same program, split three ways
│       ├── PointsModel.java            data + logic, NO Swing imports
│       ├── CanvasView.java             reads the model and draws
│       └── CanvasController.java       mouse + buttons, owns main()
├── digitiser/                 "Campus Digitiser" — the week's ideas in one app
│   ├── model/      FeatureType, GeoFeature, GeoReference, MapModel
│   ├── io/         WktFile          read/write geometry as WKT text
│   ├── view/       MapView          draws the basemap + features
│   └── controller/ MapController    ImageIO basemap, mouse, buttons, main()
└── sir/                       your assignment template (see below)

1. mvc/ — Model–View–Controller, before and after

The problem. MonolithicCanvas.java is last week’s click-to-draw canvas: a single JPanel that holds the data, draws it, and handles the mouse and buttons. Three different jobs in one class means three different reasons to change it — and the Clear button even reaches straight into the data with canvas.points.clear().

The fix. The after/ package is the same program, split into three classes with one responsibility each. Run either version — they behave identically on screen — but now changing the look touches only the View, and changing what a click means touches only the Controller.

Model — data and logic, with the iron rule: no java.awt.*, no javax.swing.*.

package at.ac.univie.gis.week8.mvc.after;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * MODEL (the "M" in MVC). Holds the data and the logic that operates on it,
 * and NOTHING else. Here the "data" is just the list of clicked points.
 *
 * THE IRON RULE: a Model must not import from java.awt.* or javax.swing.*.
 * That is also why each point is a plain int[]{x, y} rather than
 * java.awt.Point — pulling in java.awt.Point would chain the Model to the GUI.
 */
public class PointsModel {

    private final List<int[]> points = new ArrayList<>();

    /** Add a point at pixel position (x, y). */
    public void add(int x, int y) {
        points.add(new int[]{x, y});
    }

    /** Remove every point. */
    public void clear() {
        points.clear();
    }

    /** How many points we currently hold. */
    public int size() {
        return points.size();
    }

    /**
     * Hand the View a READ-ONLY view of the points. Only the Model changes its
     * own data — every real change goes through add()/clear().
     */
    public List<int[]> getPoints() {
        return Collections.unmodifiableList(points);
    }
}

View — reads the model and draws. No data of its own, no listeners.

package at.ac.univie.gis.week8.mvc.after;

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;

/**
 * VIEW (the "V" in MVC). Its only job is to DRAW the model. It is given a
 * reference to the model and reads from it; it does not own the data and it
 * does not handle input.
 */
public class CanvasView extends JPanel {

    private static final int DOT_RADIUS = 6;

    private final PointsModel model;

    public CanvasView(PointsModel model) {
        this.model = model;
        setPreferredSize(new Dimension(600, 500));
        setBackground(Color.WHITE);
    }

    /**
     * Swing calls this whenever the panel must be (re)drawn — never call it
     * directly. After the data changes the Controller calls repaint(), and Swing
     * then calls this method, redrawing from the current model state.
     */
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);

        List<int[]> points = model.getPoints(); // read from the model

        g2d.setColor(Color.BLUE);
        for (int i = 1; i < points.size(); i++) {
            int[] a = points.get(i - 1);
            int[] b = points.get(i);
            g2d.drawLine(a[0], a[1], b[0], b[1]);
        }

        g2d.setColor(Color.RED);
        for (int[] p : points) {
            g2d.fillOval(p[0] - DOT_RADIUS, p[1] - DOT_RADIUS,
                         DOT_RADIUS * 2, DOT_RADIUS * 2);
        }
    }
}

Controller — the glue, and the program’s entry point. The click cycle is the whole of interactive programming: listen → update the model → ask the view to repaint.

package at.ac.univie.gis.week8.mvc.after;

import java.awt.BorderLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * CONTROLLER (the "C" in MVC). Also the program's entry point. Glue between
 * Model and View: listens for input, updates the Model, asks the View to
 * repaint. Compare clear() with the monolithic version — here it calls a model
 * METHOD (model.clear()) instead of reaching into the model's fields.
 */
public class CanvasController {

    private final PointsModel model;
    private final CanvasView view;

    public CanvasController(PointsModel model, CanvasView view) {
        this.model = model;
        this.view = view;

        view.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                model.add(e.getX(), e.getY()); // 1. update the model
                view.repaint();                 // 2. ask the view to refresh
            }
        });
    }

    public void start() {
        JButton clear = new JButton("Clear");
        clear.addActionListener(e -> {
            model.clear();   // go through the model's public method...
            view.repaint();  // ...then refresh the screen
        });

        JPanel controls = new JPanel();
        controls.add(clear);

        JFrame frame = new JFrame("MVC Canvas (after): click to draw");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());
        frame.add(view, BorderLayout.CENTER);
        frame.add(controls, BorderLayout.SOUTH);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        // The one place that knows all three parts: create, connect, start.
        PointsModel model = new PointsModel();
        CanvasView view = new CanvasView(model);
        CanvasController controller = new CanvasController(model, view);
        controller.start();
    }
}

2. digitiser/ — the Campus Digitiser

One small app, every Week-8 idea in one place. Load a map of the area around the NIG building, click to trace your walk to campus (a route → WKT LINESTRING) or a building outline (a footprint → WKT POLYGON), then save your work to a file and load it back. It is strict MVC: the model holds geometry in lon/lat with no Swing; the view draws the basemap and features; the controller handles input and loads the basemap with ImageIO. A missing basemap, or a corrupt line in a saved file, never crashes it — it falls back or skips. Run controller.MapController.

The file-I/O and exception story lives in one small class, io/WktFile.java — writing with FileWriter/BufferedWriter in a try-with-resources block, reading with BufferedReader.readLine(), and two recovery patterns (fall back, or skip a bad line):

package at.ac.univie.gis.week8.digitiser.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import at.ac.univie.gis.week8.digitiser.model.GeoFeature;

/**
 * Reads and writes digitised features as text — one WKT geometry per line.
 * The whole week's file-I/O and exception story in one class.
 */
public final class WktFile {

    private WktFile() { }

    /**
     * Write every feature to 'path', one WKT line each. try-with-resources
     * closes the file automatically, even if writing throws — the modern
     * replacement for a manual finally block. We let IOException propagate so
     * the controller (which has the UI) can decide how to report it.
     */
    public static void save(List<GeoFeature> features, String path) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(
                new FileWriter(path, StandardCharsets.UTF_8))) {
            for (GeoFeature feature : features) {
                writer.write(feature.toWkt());
                writer.newLine();
            }
        } // writer.close() happens here automatically
    }

    /**
     * Read features back from 'path'. Two recovery patterns:
     *   FALLBACK — file missing/unreadable -> return an empty list.
     *   SKIP     — one corrupt line -> log it and keep reading the rest.
     */
    public static List<GeoFeature> load(String path) {
        List<GeoFeature> features = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(
                new FileReader(path, StandardCharsets.UTF_8))) {

            String line;
            while ((line = reader.readLine()) != null) {
                if (line.isBlank()) continue;
                try {
                    features.add(GeoFeature.parseWkt(line));
                } catch (IllegalArgumentException bad) {
                    // one bad row must not sink the whole load
                    System.err.println("Skipping bad line: " + bad.getMessage());
                }
            }
        } catch (IOException e) {
            System.err.println("Could not read " + path + "; starting empty.");
        }
        return features;
    }
}

Mandatory Assignment

SIR Epidemic Simulation (Individual · 10% of the final grade)

A small town lives on a grid. Each cell is empty, or holds one person who is Susceptible, Infectious, or Recovered. Every day, people near the sick may catch the disease, the sick slowly recover, and everyone wanders to nearby empty cells — unless a stay-at-home order keeps them in. The model is inspired by agent-based epidemic simulations (Huang et al., Simulating SARS, 2004). Your question to explore: can strict stay-at-home orders effectively contain the epidemic?

You get a working template with the MVC structure, the GUI controls and their wiring, a Timer, random seeding, the people-movement logic, and a live S/I/R chart already built. You complete four key parts (search for TODO):

  1. countInfectiousNeighbours(row, col) — count the Infectious cells among the 8 Moore neighbours (just like the Game of Life neighbour count).
  2. nextState(current, infectiousNeighbours) — the SIR rule: S→I with probability 1 − (1 − β)k, I→R with probability γ, R stays R.
  3. step() — advance one day with the copy-and-swap pattern: build a new grid from the current one (using the two methods above), then swap it in.
  4. the GUI layout — arrange the sliders, buttons, the map and the chart with a layout manager. Make the interface your own.

Submission: upload FirstNameLastNameSIR.zip with all your .java files. Individual work.
Deadline: July 31st, 2026 at 23:00 (Vienna Time).