To render the spatial map of rooms as an EPS (Encapsulated PostScript) file.


§1.

void RenderEPSMap::prepare_universe(inter_tree *I) {
    Create the main EPS map super-level1.1;
    for (int z=Universe.corner1.z; z>=Universe.corner0.z; z--)
        Create an EPS map level for this z-slice1.2;

    IXInstances::decode_hints(I, 2);
    if (changed_global_room_colour == FALSE)
        Inherit EPS room colours from those used in the World Index1.3;
}

void RenderEPSMap::render_map_as_EPS(filename *F) {
    inter_tree *I = Index::get_tree();
    RenderEPSMap::prepare_universe(I);
    Open a stream and write the EPS map to it1.4;
}

§1.1. Create the main EPS map super-level1.1 =

    EPS_map_level *main_eml = CREATE(EPS_map_level);
    main_eml->width = ConfigureIndexMap::get_int_mp(I"minimum-map-width", NULL);
    main_eml->actual_height = 0;
    main_eml->titling_point_size = ConfigureIndexMap::get_int_mp(I"title-size", NULL);
    main_eml->titling = Str::new();
    WRITE_TO(main_eml->titling, "Map");
    main_eml->contains_titling = TRUE;
    main_eml->contains_rooms = FALSE;
    ConfigureIndexMap::prepare_map_parameter_scope(&(main_eml->map_parameters));
    ConfigureIndexMap::put_text_mp(I"title", &(main_eml->map_parameters), main_eml->titling);

§1.2. Create an EPS map level for this z-slice1.2 =

    EPS_map_level *eml = CREATE(EPS_map_level);
    eml->contains_rooms = TRUE;
    eml->map_level = z;

    eml->y_max = -100000, eml->y_min = 100000;
    faux_instance *R;
    LOOP_OVER_ROOMS(R)
        if (Room_position(R).z == z) {
            if (Room_position(R).y < eml->y_min) eml->y_min = Room_position(R).y;
            if (Room_position(R).y > eml->y_max) eml->y_max = Room_position(R).y;
        }

    Str::clear(eml->titling);
    char *level_rubric = "Map"; int par = 0;
    PL::HTMLMap::devise_level_rubric(z, &level_rubric, &par);
    WRITE_TO(eml->titling, level_rubric, par);

    if (Str::len(eml->titling) == 0) eml->contains_titling = FALSE;
    else eml->contains_titling = TRUE;

    ConfigureIndexMap::prepare_map_parameter_scope(&(eml->map_parameters));
    ConfigureIndexMap::put_text_mp(I"subtitle", &(eml->map_parameters), eml->titling);

    LOOP_OVER_ROOMS(R)
        if (Room_position(R).z == z) {
            IXInstances::get_parameters(R)->wider_scope = &(eml->map_parameters);
        }

§1.3. Inherit EPS room colours from those used in the World Index1.3 =

    faux_instance *R;
    LOOP_OVER_ROOMS(R)
        ConfigureIndexMap::put_text_mp(I"room-colour", IXInstances::get_parameters(R),
            R->fimd.colour);

§1.4. Open a stream and write the EPS map to it1.4 =

    text_stream EPS_struct; text_stream *EPS = &EPS_struct;
    if (STREAM_OPEN_TO_FILE(EPS, F, ISO_ENC) == FALSE) {
        #ifdef CORE_MODULE
        Problems::fatal_on_file("Can't open index file", F);
        #endif
        #ifndef CORE_MODULE
        Errors::fatal_with_file("can't open index file", F);
        #endif
    }
    RenderEPSMap::EPS_compile_map(EPS);
    STREAM_CLOSE(EPS);

§2.

void RenderEPSMap::EPS_compile_map(OUTPUT_STREAM) {
    int blh,  total height of the EPS map area (not counting border)
        blw,  total width of the EPS map area (not counting border)
        border = ConfigureIndexMap::get_int_mp(I"border-size", NULL),
        vskip = ConfigureIndexMap::get_int_mp(I"vertical-spacing", NULL);
    Compute the dimensions of the EPS map2.1;
    int bounding_box_width = blw+2*border, bounding_box_height = blh+2*border;

    RenderEPSMap::EPS_compile_header(OUT, bounding_box_width, bounding_box_height,
        ConfigureIndexMap::get_text_mp(I"title-font", NULL), ConfigureIndexMap::get_int_mp(I"title-size", NULL));

    if (ConfigureIndexMap::get_int_mp(I"map-outline", NULL))
        Draw a big rectangular outline around the entire EPS map2.2;

    EPS_map_level *eml;
    LOOP_OVER(eml, EPS_map_level) {
        map_parameter_scope *level_scope = &(eml->map_parameters);
        int mapunit = ConfigureIndexMap::get_int_mp(I"grid-size", level_scope);
        if (eml->contains_rooms == FALSE)
            if (ConfigureIndexMap::get_int_mp(I"map-outline", NULL))
                Draw an intermediate strut in the big rectangular outline2.3;
        if (eml->contains_titling)
            Draw the title for this EPS map level2.4;
        if (eml->contains_rooms) {
            faux_instance *R;
            LOOP_OVER_ROOMS(R)
                if (Room_position(R).z == eml->map_level)
                    Establish EPS coordinates for this room2.5;
            LOOP_OVER_ROOMS(R)
                if (Room_position(R).z == eml->map_level)
                    Draw the map connections from this room as EPS paths2.6;
            LOOP_OVER_ROOMS(R)
                if (Room_position(R).z == eml->map_level)
                    Draw the boxes for the rooms themselves2.7;
        }
    }

    Plot all of the rubrics onto the EPS map2.8;
}

§2.1. Compute the dimensions of the EPS map2.1 =

    int total_chunk_height = 0, max_chunk_width = 0;
    EPS_map_level *eml;
    LOOP_BACKWARDS_OVER(eml, EPS_map_level) {
        map_parameter_scope *level_scope = &(eml->map_parameters);
        int mapunit = ConfigureIndexMap::get_int_mp(I"grid-size", level_scope);
        int p = ConfigureIndexMap::get_int_mp(I"title-size", level_scope);
        if (eml->contains_rooms) p = ConfigureIndexMap::get_int_mp(I"subtitle-size", level_scope);
        eml->titling_point_size = p;
        eml->width = (Universe.corner1.x-Universe.corner0.x+2)*mapunit;
        if (eml->allocation_id == 0) eml->actual_height = 0;
        else eml->actual_height = (eml->y_max-eml->y_min+1)*mapunit;
        eml->eps_origin = total_chunk_height + border;
        eml->height = eml->actual_height + vskip;
        if (eml->contains_rooms) eml->height += vskip;
        if (eml->contains_titling) eml->height += eml->titling_point_size+vskip;
        total_chunk_height += eml->height;
        if (max_chunk_width < eml->width) max_chunk_width = eml->width;
    }
    blh = total_chunk_height;
    blw = max_chunk_width;

§2.2. The outline is a little like drawing the shape of a bookcase: there's a big rectangle around the whole thing...

Draw a big rectangular outline around the entire EPS map2.2 =

    WRITE("newpath %% Ruled outline outer box of map\n");
    RenderEPSMap::EPS_compile_rectangular_path(OUT, border, border, border+blw, border+blh);
    WRITE("stroke\n");

§2.3. ...and then there are horizontal shelves dividing it into compartments. (Each map level will be drawn inside one of these compartments.)

Draw an intermediate strut in the big rectangular outline2.3 =

    WRITE("newpath %% Ruled horizontal line\n");
    RenderEPSMap::EPS_compile_horizontal_line_path(OUT, border, blw+border, eml->eps_origin);
    WRITE("stroke\n");

§2.4. Draw the title for this EPS map level2.4 =

    int y = eml->eps_origin + vskip + eml->actual_height;
    if (eml->contains_rooms) {
        if (ConfigureIndexMap::get_int_mp(I"monochrome", level_scope)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 0);
        else RenderEPSMap::EPS_compile_set_colour(OUT, ConfigureIndexMap::get_text_mp(I"subtitle-colour", level_scope));
        RenderEPSMap::plot_stream_at(OUT,
            ConfigureIndexMap::get_text_mp(I"subtitle", level_scope),
            NULL, 128,
            ConfigureIndexMap::get_text_mp(I"subtitle-font", level_scope),
            border*2, y+vskip,
            ConfigureIndexMap::get_int_mp(I"subtitle-size", level_scope),
            FALSE, FALSE);
    } else {
        if (ConfigureIndexMap::get_int_mp(I"monochrome", level_scope)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 0);
        else RenderEPSMap::EPS_compile_set_colour(OUT, ConfigureIndexMap::get_text_mp(I"title-colour", level_scope));
        RenderEPSMap::plot_stream_at(OUT,
            ConfigureIndexMap::get_text_mp(I"title", NULL),
            NULL, 128,
            ConfigureIndexMap::get_text_mp(I"title-font", level_scope),
            border*2, y+2*vskip,
            ConfigureIndexMap::get_int_mp(I"title-size", level_scope),
            FALSE, TRUE);
    }

§2.5. Establish EPS coordinates for this room2.5 =

    map_parameter_scope *room_scope = IXInstances::get_parameters(R);
    int bx = Room_position(R).x-Universe.corner0.x;
    int by = Room_position(R).y-eml->y_min;
    int offs = ConfigureIndexMap::get_int_mp(I"room-offset", room_scope);
    int xpart = offs%10000, ypart = offs/10000;
    while (xpart > 5000) xpart-=10000;
    while (xpart < -5000) xpart+=10000;

    bx = (bx)*mapunit + border + mapunit/2;
    by = (by)*mapunit + eml->eps_origin + vskip + mapunit/2;

    bx += xpart*mapunit/100;
    by += ypart*mapunit/100;

    R->fimd.eps_x = bx;
    R->fimd.eps_y = by;

§2.6. Draw the map connections from this room as EPS paths2.6 =

    map_parameter_scope *room_scope = IXInstances::get_parameters(R);
    RenderEPSMap::EPS_compile_line_width_setting(OUT, ConfigureIndexMap::get_int_mp(I"route-thickness", room_scope));

    int bx = R->fimd.eps_x;
    int by = R->fimd.eps_y;
    int boxsize = ConfigureIndexMap::get_int_mp(I"room-size", room_scope)/2;
    int R_stiffness = ConfigureIndexMap::get_int_mp(I"route-stiffness", room_scope);
    int dir;
    LOOP_OVER_STORY_DIRECTIONS(dir) {
        faux_instance *T = PL::SpatialMap::room_exit(R, dir, NULL);
        int exit = story_dir_to_page_dir[dir];
        if (IXInstances::is_a_room(T))
            Draw a single map connection as an EPS arrow2.6.1;
    }
    RenderEPSMap::EPS_compile_line_width_unsetting(OUT);

§2.6.1. Draw a single map connection as an EPS arrow2.6.1 =

    int T_stiffness = ConfigureIndexMap::get_int_mp(I"route-stiffness", IXInstances::get_parameters(T));
    if (ConfigureIndexMap::get_int_mp(I"monochrome", level_scope)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 0);
    else RenderEPSMap::EPS_compile_set_colour(OUT, ConfigureIndexMap::get_text_mp(I"route-colour", level_scope));
    if ((Room_position(T).z == Room_position(R).z) &&
        (PL::SpatialMap::room_exit(T, PL::SpatialMap::opposite(dir), FALSE) == R))
        Draw a two-ended arrow for a two-way horizontal connection2.6.1.1
    else
        Draw a one-way arrow for a distant or off-level connection2.6.1.2;

§2.6.1.1. We don't want to draw this twice (once for R, once for T), so we draw it just for the earlier-defined room.

Draw a two-ended arrow for a two-way horizontal connection2.6.1.1 =

    if (R->allocation_id <= T->allocation_id)
        RenderEPSMap::EPS_compile_Bezier_curve(OUT,
            R_stiffness*mapunit, T_stiffness*mapunit,
            bx, by, exit,
            T->fimd.eps_x, T->fimd.eps_y, PL::SpatialMap::opposite(exit));

§2.6.1.2. A one-way arrow has the destination marked on it textually, since it doesn't actually go there in any visual way.

Draw a one-way arrow for a distant or off-level connection2.6.1.2 =

    int scaled = 1;
    vector E = PL::SpatialMap::direction_as_vector(exit);
    switch(exit) {
        case 8:  E = U_vector_EPS; scaled = 2; break;
        case 9:  E = D_vector_EPS; scaled = 2; break;
        case 10: E = IN_vector_EPS; scaled = 2; break;
        case 11: E = OUT_vector_EPS; scaled = 2; break;
    }
    RenderEPSMap::EPS_compile_dashed_arrow(OUT, boxsize/scaled, E, bx, by);
    RenderEPSMap::plot_text_at(OUT, NULL, T,
        ConfigureIndexMap::get_int_mp(I"annotation-length", NULL),
        ConfigureIndexMap::get_text_mp(I"annotation-font", NULL),
        bx+E.x*boxsize*6/scaled/5, by+E.y*boxsize*6/scaled/5,
        ConfigureIndexMap::get_int_mp(I"annotation-size", NULL),
        TRUE, TRUE);

§2.7. Draw the boxes for the rooms themselves2.7 =

    map_parameter_scope *room_scope = IXInstances::get_parameters(R);
    int bx = R->fimd.eps_x;
    int by = R->fimd.eps_y;
    int boxsize = ConfigureIndexMap::get_int_mp(I"room-size", room_scope)/2;
    Draw the filled box for the room2.7.1;
    Draw the outline of the box for the room2.7.2;
    Write in the name of the room2.7.3;

§2.7.1. Draw the filled box for the room2.7.1 =

    WRITE("newpath %% Room interior\n");
    if (ConfigureIndexMap::get_int_mp(I"monochrome", room_scope)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 75);
    else RenderEPSMap::EPS_compile_set_colour(OUT, ConfigureIndexMap::get_text_mp(I"room-colour", room_scope));
    RenderEPSMap::EPS_compile_room_boundary_path(OUT, bx, by, boxsize, ConfigureIndexMap::get_text_mp(I"room-shape", room_scope));
    WRITE("fill\n\n");

§2.7.2. Draw the outline of the box for the room2.7.2 =

    if (ConfigureIndexMap::get_int_mp(I"room-outline", room_scope)) {
        RenderEPSMap::EPS_compile_line_width_setting(OUT, ConfigureIndexMap::get_int_mp(I"room-outline-thickness", room_scope));
        WRITE("newpath %% Room outline\n");
        if (ConfigureIndexMap::get_int_mp(I"monochrome", level_scope)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 0);
        else RenderEPSMap::EPS_compile_set_colour(OUT, ConfigureIndexMap::get_text_mp(I"room-outline-colour", room_scope));
        RenderEPSMap::EPS_compile_room_boundary_path(OUT, bx, by, boxsize, ConfigureIndexMap::get_text_mp(I"room-shape", room_scope));
        WRITE("stroke\n");
        RenderEPSMap::EPS_compile_line_width_unsetting(OUT);
    }

§2.7.3. Write in the name of the room2.7.3 =

    int offs = ConfigureIndexMap::get_int_mp(I"room-name-offset", room_scope);
    int xpart = offs%10000, ypart = offs/10000;
    while (xpart > 5000) xpart-=10000;
    while (xpart < -5000) xpart+=10000;
    bx += xpart*mapunit/100;
    by += ypart*mapunit/100;

    if (ConfigureIndexMap::get_int_mp(I"monochrome", level_scope)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 0);
    else RenderEPSMap::EPS_compile_set_colour(OUT, ConfigureIndexMap::get_text_mp(I"room-name-colour", room_scope));
    text_stream *legend = ConfigureIndexMap::get_text_mp(I"room-name", room_scope);
    faux_instance *room_to_name = NULL;
    if (Str::len(legend) == 0) { room_to_name = R; legend = NULL; }
    RenderEPSMap::plot_text_at(OUT, legend, room_to_name,
        ConfigureIndexMap::get_int_mp(I"room-name-length", room_scope),
        ConfigureIndexMap::get_text_mp(I"room-name-font", room_scope),
        bx, by, ConfigureIndexMap::get_int_mp(I"room-name-size", room_scope),
        TRUE, TRUE);

§2.8. Plot all of the rubrics onto the EPS map2.8 =

    rubric_holder *rh;
    LOOP_OVER(rh, rubric_holder) {
        int bx = 0, by = 0;
        int xpart = rh->at_offset%10000, ypart = rh->at_offset/10000;
        int mapunit = ConfigureIndexMap::get_int_mp(I"grid-size", NULL);
        while (xpart > 5000) xpart-=10000;
        while (xpart < -5000) xpart+=10000;
        if (ConfigureIndexMap::get_int_mp(I"monochrome", NULL)) RenderEPSMap::EPS_compile_set_greyscale(OUT, 0);
        else RenderEPSMap::EPS_compile_set_colour(OUT, rh->colour);
        faux_instance *O = rh->offset_from;
        if (O) {
            bx = O->fimd.eps_x;
            by = O->fimd.eps_y;
        }
        bx += xpart*mapunit/100; by += ypart*mapunit/100;
        RenderEPSMap::plot_text_at(OUT, rh->annotation, NULL, 128, rh->font, bx, by, rh->point_size,
            TRUE, TRUE);  centred both horizontally and vertically
    }

§3. Writing text in EPS. All of words written on the map — titles, labels for arrows, rubrics, and so on — come from here.

define MAX_EPS_TEXT_LENGTH 1000
define MAX_EPS_ABBREVIATED_LENGTH MAX_EPS_TEXT_LENGTH
void RenderEPSMap::plot_text_at(OUTPUT_STREAM, text_stream *text_to_plot, faux_instance *I, int abbrev_to,
    text_stream *font, int x, int y, int pointsize, int centre_h, int centre_v) {
    TEMPORARY_TEXT(txt)
    if (text_to_plot) {
        WRITE_TO(txt, "%S", text_to_plot);
    } else if (I) {
        Try taking the name from the printed name property of the room3.1;
        If that fails, try taking the name from its source text name3.2;
    } else return;
    RenderEPSMap::plot_stream_at(OUT, txt, I, abbrev_to, font, x, y, pointsize, centre_h, centre_v);
    DISCARD_TEXT(txt)
}

§3.1. Try taking the name from the printed name property of the room3.1 =

    if (Str::len(I->printed_name) > 0) {
        WRITE_TO(txt, "%S", I->printed_name);
    }

§3.2. If that fails, try taking the name from its source text name3.2 =

    if (Str::len(txt) == 0) {
        text_stream *N = IXInstances::get_name(I);
        if (Str::len(N)) return;
        WRITE_TO(txt, "%S", N);
    }

§4.

void RenderEPSMap::plot_stream_at(OUTPUT_STREAM, text_stream *text_to_plot, faux_instance *I, int abbrev_to,
    text_stream *font, int x, int y, int pointsize, int centre_h, int centre_v) {
    TEMPORARY_TEXT(txt)
    Str::copy(txt, text_to_plot);
    Abbreviate the text to be printed by stripping dispensable letters4.1;
    RenderEPSMap::EPS_compile_text(OUT, txt, x, y, font, pointsize, centre_h, centre_v);
    DISCARD_TEXT(txt)
}

§4.1. The following cuts the text down to the abbreviation length by knocking out, in sequence: (a) lower-case vowels; (b) spaces; (c) lower-case consonants; (d) punctuation marks. If that doesn't do it, the text is simply truncated. For example, "Peisey-Nancroix" abbreviated to 10 is "Pesy-Nncrx" and to 5 is "PsyNn".

Abbreviate the text to be printed by stripping dispensable letters4.1 =

    if (abbrev_to > MAX_EPS_ABBREVIATED_LENGTH) abbrev_to = MAX_EPS_ABBREVIATED_LENGTH;
    while (Str::len(txt) > abbrev_to) {
        int j;
        for (j=Str::len(txt)-1; j>=0; j--)
            if (Characters::vowel(Str::get_at(txt, j))) goto RemoveOne;
        for (j=Str::len(txt)-1; j>=0; j--)
            if (Str::get_at(txt, j) == ' ') goto RemoveOne;
        for (j=Str::len(txt)-1; j>=0; j--)
            if (islower(Str::get_at(txt, j))) goto RemoveOne;
        for (j=Str::len(txt)-1; j>=0; j--)
            if (isupper(Str::get_at(txt, j)) == FALSE) goto RemoveOne;
        Str::truncate(txt, abbrev_to);
        break;
        RemoveOne: Str::delete_nth_character(txt, j);
    }

§5. EPS header. EPS files are identified and version-numbered by a header, as follows.

void RenderEPSMap::EPS_compile_header(OUTPUT_STREAM, int bounding_box_width, int bounding_box_height,
    text_stream *default_font, int default_point_size) {
    WRITE("%%!PS-Adobe EPSF-3.0\n");
    WRITE("%%%%BoundingBox: 0 0 %d %d\n", bounding_box_width, bounding_box_height);
    WRITE("%%%%IncludeFont: %S\n", default_font);
    WRITE("/%S findfont %d scalefont setfont\n", default_font, default_point_size);
}

§6. Circles and rectangles. In EPS files, there's an imaginary pen which traces out "paths". These begin whenever the pen moves to a new location, and then continue until they are closed (joined up back to the start position) with a closepath command.

void RenderEPSMap::EPS_compile_circular_path(OUTPUT_STREAM, int x0, int y0, int radius) {
    WRITE("%d %d moveto %% rightmost point\n", x0+radius, y0);
    WRITE("%d %d %d %d %d arc %% full circle traced anticlockwise\n",
        x0, y0, radius, 0, 360);
    WRITE("closepath\n");
}

void RenderEPSMap::EPS_compile_rectangular_path(OUTPUT_STREAM, int x0, int y0, int x1, int y1) {
    WRITE("%d %d moveto %% bottom left corner\n", x0, y0);
    WRITE("%d %d lineto %% bottom side\n", x1, y0);
    WRITE("%d %d lineto %% right side\n", x1, y1);
    WRITE("%d %d lineto %% top side\n", x0, y1);
    WRITE("closepath\n");
}

§7. The boundary of a room is always one of these:

void RenderEPSMap::EPS_compile_room_boundary_path(OUTPUT_STREAM, int bx, int by, int boxsize, text_stream *shape) {
    if (Str::cmp(shape, I"square") == 0)
        RenderEPSMap::EPS_compile_rectangular_path(OUT, bx-boxsize, by-boxsize, bx+boxsize, by+boxsize);
    else if (Str::cmp(shape, I"rectangle") == 0)
        RenderEPSMap::EPS_compile_rectangular_path(OUT, bx-2*boxsize, by-boxsize, bx+2*boxsize, by+boxsize);
    else if (Str::cmp(shape, I"circle") == 0)
        RenderEPSMap::EPS_compile_circular_path(OUT, bx, by, boxsize);
    else
        RenderEPSMap::EPS_compile_rectangular_path(OUT, bx-boxsize, by-boxsize, bx+boxsize, by+boxsize);
}

§8. Straight lines.

void RenderEPSMap::EPS_compile_horizontal_line_path(OUTPUT_STREAM, int x0, int x1, int y) {
    WRITE("%d %d moveto %% LHS\n", x0, y);
    WRITE("%d %d lineto %% RHS\n", x1, y);
    WRITE("closepath\n");
}

§9. Dashed arrows.

void RenderEPSMap::EPS_compile_dashed_arrow(OUTPUT_STREAM, int length, vector Dir, int x0, int y0) {
    WRITE("[2 1] 0 setdash %% dashed line for arrow\n");
    WRITE("%d %d moveto %% room centre\n", x0, y0);
    WRITE("%d %d rlineto %% arrow out\n", Dir.x*length, Dir.y*length);
    WRITE("stroke\n");
    WRITE("[] 0 setdash %% back to normal solid lines\n");
}

§10. Bezier curves. The other sort of path we'll need is a Bézier curve, a quadratic curve which interpolates between vectors. EPS has support for these built-in; see any reference book on PostScript.

void RenderEPSMap::EPS_compile_Bezier_curve(OUTPUT_STREAM, int stiffness0, int stiffness1,
    int x0, int y0, int exit0, int x1, int y1, int exit1) {
    int cx1, cy1, cx2, cy2;
    vector E = PL::SpatialMap::direction_as_vector(exit0);
    cx1 = x0+E.x*stiffness0/100; cy1 = y0+E.y*stiffness0/100;
    E = PL::SpatialMap::direction_as_vector(exit1);
    cx2 = x1+E.x*stiffness1/100; cy2 = y1+E.y*stiffness1/100;
    WRITE("%d %d moveto %% start of Bezier curve\n", x0, y0);
    WRITE("%d %d %d %d %d %d curveto %% control points 1, 2 and end\n",
        cx1, cy1, cx2, cy2, x1, y1);
    WRITE("stroke\n");
}

§11. Line thickness. The following routines should be used in nested pairs, so that the PostScript stack is kept in order.

void RenderEPSMap::EPS_compile_line_width_setting(OUTPUT_STREAM, int new) {
    WRITE("currentlinewidth %% Push old line width onto stack\n");
    WRITE("%d setlinewidth\n", new);
}

void RenderEPSMap::EPS_compile_line_width_unsetting(OUTPUT_STREAM) {
    WRITE("setlinewidth %% Pull old line width from stack\n");
}

§12. Text. In EPS world, text is just another sort of path.

void RenderEPSMap::EPS_compile_text(OUTPUT_STREAM, text_stream *text, int x, int y,
    text_stream *font, int pointsize, int centre_h, int centre_v) {
    WRITE("/%S findfont %d scalefont setfont\n", font, pointsize);
    WRITE("newpath (%S)\n", text);
    if (centre_h) WRITE("dup stringwidth add 2 div %d exch sub %% = X centre-offset\n", x);
    else WRITE("%d %% = X\n", x);
    if (centre_v) WRITE("%d %d 2 div sub %% = Y centre-offset\n", y, pointsize);
    else WRITE("%d %% = Y\n", y);
    WRITE("moveto show\n");
}

§13. RGB colours. Inform internally stores colours as six hexadecimal digits, in traditional HTML way: RRGGBB, with each colour from 0 to 255. In EPS files, colours are written as triples of floating point numbers \(0 \leq b \leq 1\).

EPS uses reverse Polish notation, so the command here is: R G B setrgbcolor.

void RenderEPSMap::EPS_compile_set_colour(OUTPUT_STREAM, text_stream *htmlcolour) {
    if (Str::len(htmlcolour) != 6) internal_error("Improper HTML colour");
    RenderEPSMap::choose_colour_beam(OUT, Str::get_at(htmlcolour, 0), Str::get_at(htmlcolour, 1));
    RenderEPSMap::choose_colour_beam(OUT, Str::get_at(htmlcolour, 2), Str::get_at(htmlcolour, 3));
    RenderEPSMap::choose_colour_beam(OUT, Str::get_at(htmlcolour, 4), Str::get_at(htmlcolour, 5));
    WRITE("setrgbcolor %% From HTML colour %S\n", htmlcolour);
}

void RenderEPSMap::choose_colour_beam(OUTPUT_STREAM, int hex1, int hex2) {
    int k = RenderEPSMap::hex_to_int(hex1)*16 + RenderEPSMap::hex_to_int(hex2);
    WRITE("%.6g ", (double) (((float) k)/255.0));
}

int RenderEPSMap::hex_to_int(int hex) {
    switch(hex) {
        case '0': return 0;
        case '1': return 1;
        case '2': return 2;
        case '3': return 3;
        case '4': return 4;
        case '5': return 5;
        case '6': return 6;
        case '7': return 7;
        case '8': return 8;
        case '9': return 9;
        case 'a': case 'A': return 10;
        case 'b': case 'B': return 11;
        case 'c': case 'C': return 12;
        case 'd': case 'D': return 13;
        case 'e': case 'E': return 14;
        case 'f': case 'F': return 15;
        default: internal_error("Improper character in HTML colour");
    }
    return 0;
}

§14. EPS also supports greyscale, where there's only one beam:

void RenderEPSMap::EPS_compile_set_greyscale(OUTPUT_STREAM, int N) {
    WRITE("%0.02f setgray %% greyscale %d/100ths of white\n", (float) N/100, N);
}