/
tmap_save.R
372 lines (337 loc) · 13 KB
/
tmap_save.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
#' Save tmap
#'
#' Save tmap to a file. This can be either a static plot (e.g. png) or an interactive map (html).
#'
#' @param tm tmap object
# The docs for filename could be put in tmap_mode.
#' @param filename filename including extension, and optionally the path.
#' The extensions pdf, eps, svg, wmf (Windows only), png, jpg, bmp, tiff, and html are supported.
#' If the extension is missing, the file will be saved as a static plot in `"plot"`
#' mode and as an interactive map (html) in `"view"` mode (see details).
#' The default format for static plots is png, but this can be changed using
#' the option `"output.format"` in [tmap_options()]. If `NA` (the default),
#' the file is saved as "tmap01" in the default format, and the number incremented
#' if the file already exists.
#' @param device graphic device to use. Either a device function
#' (e.g., [`png`][grDevices::png()] or [`cairo_pdf`][grDevices::cairo_pdf()])
#' or a text indicating selected graphic device: "pdf", "eps", "svg", "wmf" (Windows only), "png", "jpg", "bmp", "tiff".
#' If `NULL`, the graphic device is guessed based on the `filename` argument.
#' @param height,width The dimensions of the plot (not applicable for html files).
#' Units are set with the argument `units`. If one of them is not specified,
#' this is calculated using the formula asp = width / height, where asp is the
#' estimated aspect ratio of the map. If both are missing, they are set such
#' that `width * height` is equal to the option `"output.size"` in [tmap_options()].
#' This is by default 49, meaning that is the map is a square (so aspect ratio of 1)
#' both width and height are set to 7.
#' @param units units for width and height (`"in"`, `"cm"`, or `"mm"`).
#' By default, pixels (`"px"`) are used if either width or height is set to a
#' value greater than 50. Else, the units are inches (`"in"`).
#' @param dpi dots per inch. Only applicable for raster graphics. By default it
#' is set to 300, but this can be changed using the option `"output.dpi"` in [tmap_options()].
#' @param outer.margins overrides the outer.margins argument of [tm_layout()] (unless set to `NA`)
#' @param asp if specified, it overrides the asp argument of [tm_layout()].
#' **Tip**: set to `0` if map frame should be placed on the edges of the image.
#' @param scale overrides the scale argument of [tm_layout()] (unless set to `NA`)
#' @param insets_tm tmap object of an inset map, or a list of tmap objects of
#' multiple inset maps. The number of tmap objects should be equal to the number
#' of viewports specified with `insets_vp`.
#' @param insets_vp [`viewport`][grid::viewport()] of an inset map, or a list
#' of [`viewport`][grid::viewport()]s of multiple inset maps. The number of
#' viewports should be equal to the number of tmap objects specified with `insets_tm`.
#' @param add.titles add titles to leaflet object.
#' @param in.iframe should an interactive map be saved as an iframe?
#' If so, two HTML files will be saved; one small parent HTML file with the
#' iframe container, and one large child HTML file with the actual widget.
#' See [widgetframe::saveWidgetframe()] for details. By default `FALSE`,
#' which means that one large HTML file is saved (see [saveWidget()][htmlwidgets::saveWidget()]).
#' @param selfcontained when an interactive map is saved, should the resources
#' (e.g. JavaScript libraries) be contained in the HTML file? If `FALSE`, they
#' are placed in an adjacent directory (see also [htmlwidgets::saveWidget()]).
#' Note that the HTML file will often still be large when `selfcontained = FALSE`,
#' since the map data (polygons and popups), which are also contained in the HTML file,
#' usually take more space then the map resources.
#' @param verbose Deprecated. It is now controlled by the tmap option `show.messages`
#' (see [tmap_options()])
#' @inheritDotParams htmlwidgets::saveWidget
#' @inheritDotParams widgetframe::saveWidgetframe
#' @returns the filename, invisibly, if export is successful.
#' @importFrom htmlwidgets saveWidget
#' @import tmaptools
#' @example ./examples/tmap_save.R
#' @export
tmap_save = function(tm=NULL, filename=NA, device=NULL, width=NA, height=NA, units = NA,
dpi=NA, outer.margins=NA, asp=NULL, scale=NA, insets_tm=NULL,
insets_vp=NULL, add.titles = TRUE, in.iframe = FALSE, selfcontained = !in.iframe, verbose = NULL, ...) {
.tmapOptions = get("tmapOptions", envir = .TMAP)
show.warnings =.tmapOptions$show.warnings
if (!missing(verbose) && show.warnings) warning("The argument verbose is deprecated. Please use the option show.messages of tmap_options instead.")
verbose = .tmapOptions$show.messages
lastcall = x = get("last_map", envir = .TMAP)
if (missing(tm)) {
tm = suppressWarnings(tmap_last())
if (is.null(tm)) stop("A map has not been created yet")
is.arrange = FALSE
} else if (inherits(tm, "tmap")) {
is.arrange = FALSE
} else if (inherits(tm, "tmap_arrange")) {
is.arrange = TRUE
} else {
stop("Unknown format. tm should be either a tmap output, or a list of tmap outputs")
}
tmap.mode = getOption("tmap.mode")
default_ext = ifelse(tmap.mode == "plot", .tmapOptions$output.format, "html")
if (is.na(filename)) {
filename_default = paste("tmap01", default_ext, sep = ".")
if (!file.exists(filename_default)) {
filename = filename_default
} else {
files = list.files(pattern = paste0("^tmap[0-9]{2}\\.", default_ext, "$"))
fid = max(as.integer(substr(files, 5, 6))) + 1L
filename = paste0("tmap", sprintf("%02d", fid), ".", default_ext)
}
}
if (is.na(dpi)) dpi = .tmapOptions$output.dpi
on.exit({
assign("last_map", lastcall, envir = .TMAP)
})
get_ext = function(filename, default_ext) {
pieces = strsplit(filename, "\\.")[[1]]
if (length(pieces)==1) return(default_ext)
tolower(pieces[length(pieces)])
}
convert_to_inches = function(x, units) {
x = switch(units, px = x/dpi, `in` = x, cm = x/2.54, mm = x/2.54/10)
}
convert_to_pixels = function(x, units) {
x = switch(units, px = x, `in` = dpi*x, cm = dpi*x/2.54, mm = dpi*x/2.54/10)
}
ext = get_ext(filename, default_ext)
interactive = (ext=="html")
options(tmap.mode=ifelse(interactive, "view", "plot"))
if (interactive) {
if (is.arrange) {
lf = print_tmap_arrange(tm, show = FALSE, add.titles=add.titles)
} else {
lf = print.tmap(tm, show = FALSE)
}
tryCatch({
wd = getwd()
on.exit(setwd(wd), add = TRUE)
wd_new = dirname(filename)
base_filename = basename(filename)
setwd(wd_new)
if (in.iframe) {
widgetframe::saveWidgetframe(lf, file=base_filename, selfcontained = selfcontained, ...)
} else {
htmlwidgets::saveWidget(lf, file=base_filename, selfcontained = selfcontained, ...)
}
}, error = function(e) {
stop("Unable to save the interactive map. Note that saving interactive small multiples is not supported yet. The error message from htmlwidgets::saveWidget is ", call. = FALSE)
})
options(tmap.mode=tmap.mode)
if (verbose) {
message("Interactive map saved to ", suppressWarnings(normalizePath(filename)))
}
return(invisible(filename))
}
if (is.na(width) || is.na(height)) {
if (!is.na(width)) {
if (is.na(units)) units = choose_unit(width)
temp_size = convert_to_pixels(width, units)
} else if (!is.na(height)) {
if (is.na(units)) units = choose_unit(height)
temp_size = convert_to_pixels(height, units)
} else {
units = "px"
temp_size = 700
}
if (is.arrange) {
sasp = 1
} else {
show.messages = tmap_options(show.messages = FALSE)
on.exit(tmap_options(show.messages))
sasp = get_asp_ratio(tm, width = temp_size, height = temp_size, res = dpi)
tmap_options(show.messages)
}
if (is.na(width) && !is.na(height)) {
width = height * sasp
} else if (is.na(height) && !is.na(width)) {
height = width / sasp
} else {
units = "in"
height = sqrt(.tmapOptions$output.size / sasp)
width = height * sasp
}
} else {
if (is.na(units)) units = choose_unit(max(width, height))
}
units_target = ifelse(units=="px" && ext %in% c("png", "jpg", "jpeg", "bmp", "tiff"), "px", "in")
if (units_target=="in") {
width = convert_to_inches(width, units)
height = convert_to_inches(height, units)
if (ext=="pdf") {
round_to_1_72 = function(x) x %/% (1/72) / 72
width = round_to_1_72(width)
height = round_to_1_72(height)
}
} else {
width = convert_to_pixels(width, units)
height = convert_to_pixels(height, units)
}
old_dev = grDevices::dev.cur()
dev = plot_device(device = device, ext = ext, filename = filename,
dpi = dpi, units_target = units_target)
dev(filename = filename, width = width, height = height, ...)
on.exit(capture.output({
dev.off()
if (old_dev > 1) grDevices::dev.set(old_dev) # restore old device unless null device
}), add = TRUE)
if (is.arrange) {
opts = attr(tm, "opts")
if (!is.na(outer.margins[1])) opts$outer.margins = outer.margins
if (!missing(asp)) opts$asp = asp
attr(tm, "opts") = opts
print(tm)
} else {
args = list()
if (!is.na(outer.margins[1])) args$outer.margins = outer.margins
if (!missing(asp)) args$asp = asp
if (!is.na(scale)) args$scale = scale
print(tm + do.call("tm_layout", args))
}
if (!is.arrange && !missing(insets_tm) && !missing(insets_vp)) {
args_inset = if (!is.na(scale)) list(scale = scale) else list()
if (inherits(insets_tm, "list") && inherits(insets_vp, "list")) {
if (length(insets_tm) != length(insets_vp)) stop("Number of insets unequal to number of viewports")
mapply(function(tm_i, vp_i) {
print(tm_i + do.call("tm_layout", args_inset), vp=vp_i)
}, insets_tm, insets_vp)
} else if (inherits(insets_tm, "tmap") && inherits(insets_vp, "viewport")) {
print(insets_tm + do.call("tm_layout", args_inset), insets_vp)
} else {
stop("Insets and/or its viewports not in the correct format")
}
}
if (verbose) {
message("Map saved to ", suppressWarnings(normalizePath(filename)))
if (ext %in% c("png", "jpg", "jpeg", "bmp", "tiff")) {
if (units_target == "px") {
wp = format(width)
hp = format(height)
wi = format(convert_to_inches(width, "px"))
hi = format(convert_to_inches(height, "px"))
} else {
wi = format(width)
hi = format(height)
wp = format(convert_to_pixels(width, "in"))
hp = format(convert_to_pixels(height, "in"))
}
message("Resolution: ", format(wp), " by ", format(hp), " pixels")
message("Size: ", wi, " by ", hi, " inches (", format(dpi), " dpi)")
} else {
wi = format(width)
hi = format(height)
message("Size: ", wi, " by ", hi, " inches")
}
}
options(tmap.mode=tmap.mode)
invisible(filename)
}
plot_device = function(device, ext, filename, dpi, units_target){
force(filename)
force(dpi)
force(units_target)
if (is.function(device)) {
args = formals(device)
call_args = list()
if ("file" %in% names(args)) {
call_args$file = filename
}
if ("res" %in% names(args)) {
call_args$res = dpi
}
if ("units" %in% names(args)) {
call_args$units = units_target
}
dev = function(...) do.call(device, utils::modifyList(list(...), call_args))
return(dev)
}
ps = function(..., filename, width, height)
grDevices::postscript(
...,
file = filename,
width = width,
height = height,
onefile = FALSE,
horizontal = FALSE,
paper = "special"
)
jpeg = function(..., width, height)
grDevices::jpeg(
...,
width = width,
height = height,
res = dpi,
units = units_target
)
devices = list(
ps = ps,
eps = ps,
tex = function(..., filename, width, height)
grDevices::pictex(..., file = filename, width = width, height = height),
pdf = function(..., filename, version = "1.4")
grDevices::pdf(..., file = filename, version = version),
svg = function(..., filename)
grDevices::svg(..., file = filename),
wmf = function(..., width, height)
grDevices::win.metafile(..., width = width, height = height),
emf = function(..., width, height)
grDevices::win.metafile(..., width = width, height = height),
png = function(..., width, height)
grDevices::png(
...,
width = width,
height = height,
res = dpi,
units = units_target
),
jpeg = jpeg,
jpg = jpeg,
bmp =
function(..., width, height)
grDevices::bmp(
...,
width = width,
height = height,
res = dpi,
units = units_target
),
tiff = function(..., width, height)
grDevices::tiff(
...,
width = width,
height = height,
res = dpi,
units = units_target
)
)
if (is.null(device)) {
dev = devices[[ext]]
if (is.null(dev)) {
stop("'", dev, "'", " graphic device does not exist", call. = FALSE)
}
return(dev)
}
if (!device %in% names(devices)) {
stop('"', device, '"', " graphic device does not exist", call. = FALSE)
}
}
choose_unit = function(x) {
units = ifelse(x > 50, "px", "in")
if (x > 15 && x < 100) {
message("The argument 'units' has been set to \"",
units, "\" since the specified width or height is ",
ifelse(units == "px", "greater than ", "less than or equal to "), 50, ". Specify units = \"", ifelse(units == "px", "in", "px"),"\" to change this.")
}
units
}