The Roadmap Generator is a tool I wrote with the aim of procedurally generating road maps of large cities.

There are two versions:

  • first version in Python made in 2019 that produce files aimed to be compatible with my owner software QBatica; it leverages PIL and pygame graphics libraries;
  • the second Angular version was made in 2022; is equipped with more advanced algorithms and feature end in themself, but it's currently incomplete; it leverages HTML Canvas technology. This version presents an editor on which it's possible to draw cartographic maps in the style of Google Maps. It handles semi-automatically the application of rounds, crossroads and highway junctions.
Schermata del Roadmap Generator

The new version of Roadmap Generation is thought to be a Google-Maps-looking maps generator to create custom and completely made-up geographic road maps. Starting from a blank background, the user can draw 5 typologies of elements:

  • roads, that can be of 4 types rendered in different ways:
    • standard road;
    • main city road;
    • regional highway;
    • Way (highway)
  • within the roads the user can add crossroads (between two roads) and highway junctions. The junctions can be of two types:
    • X junction, that connect and interchange two highways;
    • Y junction, to graft city roads to the major roads and make up the highway exits.
  • zones, that represents what the underlying area is allocated for. Drawn in different colours, they can be of the following types:
    • wild zone, the default one;
    • agricultural zone, the areas occupied by men but not urbanized;
    • park zone, that are green areas within the urban zone representing parks and gardens;
    • water, areas that can represent rivers, seas, canals or lakes;
    • city, the urbanized zones.
  • rounds, the roundabouts that the user can place on the intersection between two roads. The roundabouts adapt themself to follow the angle of the to road interception (so adapting to non-perpendicular crossroads), while the user can choose the size of it by scrolling the mouse wheel.
  • places text items that the user can place anywhere to represent generic places, districts names or city names.
Schermata del nuovo Roadmap Generator

Roads drawing and editing

The main component of the software architecture handles a sort of state machine to define which type of operation is on the way to be performed by the user. For example, in the case of the roads, there can be the following states:

  • no operation occurring, the mouse events are simply handled to pan or zoom the map;
  • road first drawing, the mouse events are handled to draw new points on the map and append them to the road's line path;
  • road editing, the mouse events are handled to perform points translations and the insertion of new points, breaking in two the existing segments that compose the road.

The user can draw the roads by clicking on the screen to add the points that compose the road path. In the initial creation mode the user clicks point by point to draw the path; when the creation is terminated the road element's data is added to the scene "database" and then a name for the road can be chosen. After that, the user can edit the line points at any time, to fix and adjust the path and add new points to extend the road. By clicking on the road from the map or selecting it from the roads outline, the editing mode is activated. Here the user can move the points by simply dragging them or adding new ones by ctrl+clicking along the road line.

About the software architecture, every element has two logic services: the service responsible for the allocation, editing and creation of the element (which handles coordinates, types definition, selection) and the service responsible for the appearance of the element, namely the actual rendering style, based on its position, its type and eventual other properties. So, in the case of the road I wrote two services: the RoadService (extending the AbstractElementService<T>) and the RoadGraphicService.

When the canvas click event occurres in editing mode, the point coordinates are emitted, and then the service add them to the list the made up the road line. In the creation or editing stage the Graphic Service renders differently, clearly showing the control points. The main component extracts the click coordinate with the following method:

private getCursorPosition(canvas, event): Point {
    const rect = canvas.getBoundingClientRect(), // abs. size of element
      scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for X
      scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for Y

    let point: Point = {
      x: Math.round((event.clientX - rect.left) * scaleX),   // scale mouse coordinates after they have
      y: Math.round((event.clientY - rect.top) * scaleY)     // been adjusted to be relative to element
    };

    point.x = point.x / this.gs.zoom - (((this.panX * this.zoom) + this.gs.zoomShiftX) / this.gs.zoom);
    point.y = point.y / this.gs.zoom - (((this.panY * this.zoom) + this.gs.zoomShiftY) / this.gs.zoom);

    return point;
}

The RoadService, also, listens for the click on canvas to edit the road line and handles the properties of the road based on the type. The GeometryService exposes several trigonometric or geometric functions to perform some recurring operation, such as verify that a point is on a segment defined by two other points. In method below the RoadService tries to understand if the user is clicking to edit some road or less, so makes use of the isBetween() method of the GeometryService to verify if the clicked point lies along one of the segments of one of the road lines of the scene. Of course, this is performed considering a minimum possible deviation, because the human click can easily not be precise at the pixel. This threshold is defined from the service's constant ROAD_SNAP_TOLERANCE.

trySelect(point: Point): boolean {
        for (let j = 0; j < this.ds.LINES.length; j++) {
            var road = this.ds.LINES[j];
            for (let i = 0; i < road.lines.length - 1; i++) {
                const result = this.geometryService.isBetween(road.lines[i], road.lines[i + 1], point, this.ROAD_SNAP_TOLERANCE);
                if (result === true) {
                    this.edit(j);
                    return true;
                }
            }
        }
        return false;
}

If this interception occurs on the editing road state, and the CTRL key is down, then a new point is added between the lines. In the same way the service tries to understand if the user is dragging an existing point by comparing the mouse coordinates with each single point of the road line.

Rounds and junctions placing

The special feature of the highway junctions and of the roundabouts is that they are able to automatically adapt their shape to follow the angle of interception of the two roads they are joining. For example a roundabout placed on a cross intersection of 90° will be perfectly perpendicular, but a little shear will be applied to the roundabout placed on an intersection of different angle. Placing junctions and rounds the user will experiment with a sort of "snapping" of the element along the two roads.

Zones drawing and editing

Roads and districts text placements

Blocks generation

Previous Post Next Post