sugitora commited on
Commit
058b28d
·
verified ·
1 Parent(s): f82bd24

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +119 -418
app.R CHANGED
@@ -1,475 +1,176 @@
1
- # app.R - 警備マッチング可視化アプリ(改良版)
2
- # 要件: 充足率表示、日付期間選択、要営業強化エリア識別
3
 
4
  library(shiny)
5
  library(dplyr)
 
6
  library(leaflet)
7
 
8
- # =========================================================
9
- # 0) 設定
10
- # =========================================================
11
- BBOX <- list(
12
- lat_min = 36.0,
13
- lat_max = 36.9,
14
- lng_min = 139.3,
15
- lng_max = 140.3
16
- )
17
 
18
- # 充足率の閾値
19
- THRESHOLD_LOW <- 80 # 80%未満 = 要営業強化(赤)
20
- THRESHOLD_HIGH <- 100 # 100%以上 = 充足(緑)
21
 
22
- # =========================================================
23
- # 1) ユーティリティ
24
- # =========================================================
25
- read_csv_bom <- function(path) {
26
- if (!file.exists(path)) stop(paste("ファイルが見つからない:", path))
27
- df <- tryCatch(
28
- read.csv(path, fileEncoding = "UTF-8-BOM", stringsAsFactors = FALSE, check.names = FALSE),
29
- error = function(e) read.csv(path, stringsAsFactors = FALSE, check.names = FALSE)
30
- )
31
- df
32
- }
33
 
34
- to_date <- function(x) {
35
- if (inherits(x, "Date")) return(x)
36
- x <- as.character(x)
37
- x <- trimws(x)
38
- x[x == ""] <- NA
39
- x2 <- gsub("/", "-", x, fixed = TRUE)
40
- as.Date(x2)
41
- }
42
 
43
- haversine_km <- function(lat1, lon1, lat2, lon2) {
44
- r <- 6371.0
45
- to_rad <- function(x) x * pi / 180
46
- dlat <- to_rad(lat2 - lat1)
47
- dlon <- to_rad(lon2 - lon1)
48
- a <- sin(dlat/2)^2 + cos(to_rad(lat1)) * cos(to_rad(lat2)) * sin(dlon/2)^2
49
- 2 * r * asin(pmin(1, sqrt(a)))
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- extract_city <- function(addr) {
53
- if (is.na(addr) || trimws(addr) == "") return(NA_character_)
54
- a <- gsub(" ", " ", addr, fixed = TRUE)
55
- a <- gsub("\\(.*?\\)", "", a)
56
- a <- gsub("\\(.*?\\)", "", a)
57
- a2 <- sub(".*県", "", a)
58
- m <- regexpr("(市|町|村|区)", a2)
59
- if (m[1] == -1) return(NA_character_)
60
- endpos <- m[1] + attr(m, "match.length") - 1
61
- substr(a2, 1, endpos)
62
- }
63
 
64
- # 充足率に応じた色を返す
65
- get_fill_color <- function(rate) {
66
- case_when(
67
- is.na(rate) ~ "gray",
68
- rate < THRESHOLD_LOW ~ "#e74c3c", # 赤 - 要営業強化
69
- rate < THRESHOLD_HIGH ~ "#f39c12", # オレンジ - 注意
70
- TRUE ~ "#27ae60" # 緑 - 充足
71
- )
72
- }
73
-
74
- # =========================================================
75
- # 2) データ読み込み&前処理
76
- # =========================================================
77
- load_all_data <- function() {
78
- contract_raw <- read_csv_bom("contract_list.csv")
79
- guard_raw <- read_csv_bom("guard_master.csv")
80
- avail_raw <- read_csv_bom("availability.csv")
81
-
82
- # ---- contract_list ----
83
- contract <- contract_raw %>%
84
- mutate(
85
- `案件予定日(開始)` = to_date(`案件予定日(開始)`),
86
- `案件予定日(終了)` = to_date(`案件予定日(終了)`),
87
- 現場住所 = paste0(
88
- ifelse(is.na(`現場住所1`), "", `現場住所1`),
89
- ifelse(is.na(`現場住所2`) | trimws(`現場住所2`) == "", "", paste0(" ", `現場住所2`))
90
- ),
91
- required_guards = `必要人数`,
92
- site_city = `市区町村`
93
- )
94
-
95
- # ---- guard_master ----
96
- guard <- guard_raw %>%
97
- mutate(
98
- 従業員番号 = suppressWarnings(as.integer(`従業員番号`)),
99
- guard_city = vapply(`住所`, extract_city, character(1))
100
- )
101
-
102
- # ---- availability ----
103
- avail <- avail_raw %>%
104
- mutate(
105
- 日付 = to_date(`日付`),
106
- 従業員番号 = suppressWarnings(as.integer(`従業員番号`)),
107
- available_flag = as.integer(`対応可否`)
108
- )
109
-
110
- list(contract = contract, guard = guard, avail = avail)
111
- }
112
-
113
- DATA <- NULL
114
- LOAD_ERROR <- NULL
115
- tryCatch({
116
- DATA <- load_all_data()
117
- }, error = function(e) {
118
- LOAD_ERROR <<- e$message
119
- })
120
-
121
- # =========================================================
122
- # 3) UI
123
- # =========================================================
124
  ui <- fluidPage(
125
- tags$head(
126
- tags$link(rel = "icon", href = "data:,"),
127
- tags$style(HTML("
128
- .legend-box { padding: 10px; background: white; border-radius: 5px; }
129
- .legend-item { display: flex; align-items: center; margin: 5px 0; }
130
- .legend-color { width: 20px; height: 20px; border-radius: 50%; margin-right: 8px; }
131
- .summary-header { background-color: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; }
132
- "))
133
- ),
134
- titlePanel("警備マッチング可視化(充足率・期間選択対応)"),
135
  sidebarLayout(
136
  sidebarPanel(
137
- if (!is.null(LOAD_ERROR)) {
138
- tags$div(
139
- style = "color:#b00020; font-weight:600;",
140
- paste0("データ読込エラー: ", LOAD_ERROR),
141
- tags$br(),
142
- "同じフォルダに contract_list.csv / guard_master.csv / availability.csv を置いているか確認する。"
143
- )
144
- } else {
145
- tagList(
146
- h4("📅 期間選択"),
147
- sliderInput(
148
- "date_range",
149
- "対象期間",
150
- min = min(DATA$contract$`案件予定日(開始)`, na.rm = TRUE),
151
- max = max(DATA$contract$`案件予定日(終了)`, na.rm = TRUE),
152
- value = c(
153
- min(DATA$contract$`案件予定日(開始)`, na.rm = TRUE),
154
- min(DATA$contract$`案件予定日(開始)`, na.rm = TRUE) + 7
155
- ),
156
- timeFormat = "%m/%d",
157
- step = 1
158
- ),
159
- hr(),
160
- h4("🗺️ 表示設定"),
161
- checkboxInput("show_guards", "アベイラブル隊員を表示", TRUE),
162
- radioButtons(
163
- "match_mode",
164
- "マッチング方法",
165
- choices = c("市区町村一致" = "city", "距離(半径km)" = "dist"),
166
- selected = "city"
167
- ),
168
- conditionalPanel(
169
- condition = "input.match_mode == 'dist'",
170
- numericInput("radius_km", "半径(km)", value = 20, min = 1, max = 200, step = 1)
171
- ),
172
- hr(),
173
- h4("📊 凡例"),
174
- tags$div(
175
- class = "legend-box",
176
- tags$div(class = "legend-item",
177
- tags$div(class = "legend-color", style = "background-color: #27ae60;"),
178
- tags$span("充足(100%以上)")
179
- ),
180
- tags$div(class = "legend-item",
181
- tags$div(class = "legend-color", style = "background-color: #f39c12;"),
182
- tags$span("注意(80-99%)")
183
- ),
184
- tags$div(class = "legend-item",
185
- tags$div(class = "legend-color", style = "background-color: #e74c3c;"),
186
- tags$span("要営業強化(80%未満)")
187
- ),
188
- tags$div(class = "legend-item",
189
- tags$div(class = "legend-color", style = "background-color: #3498db; opacity: 0.7;"),
190
- tags$span("アベイラブル隊員")
191
- )
192
- )
193
- )
194
- },
195
  width = 3
196
  ),
197
  mainPanel(
198
- # サマリー統計
199
- fluidRow(
200
- column(3,
201
- tags$div(class = "summary-header",
202
- h5("稼働現場数"),
203
- textOutput("stat_sites", inline = TRUE)
204
- )
205
- ),
206
- column(3,
207
- tags$div(class = "summary-header",
208
- h5("必要人数合計"),
209
- textOutput("stat_required", inline = TRUE)
210
- )
211
- ),
212
- column(3,
213
- tags$div(class = "summary-header",
214
- h5("対応可能人数"),
215
- textOutput("stat_available", inline = TRUE)
216
- )
217
- ),
218
- column(3,
219
- tags$div(class = "summary-header",
220
- h5("平均充足率"),
221
- textOutput("stat_avg_rate", inline = TRUE)
222
- )
223
- )
224
- ),
225
- leafletOutput("map", height = 480),
226
  br(),
227
- h4("現場別 需給状況(選択期間)"),
228
  tableOutput("summary"),
229
  width = 9
230
  )
231
  )
232
  )
233
 
234
- # =========================================================
235
- # 4) Server
236
- # =========================================================
 
237
  server <- function(input, output, session) {
238
- if (!is.null(LOAD_ERROR)) {
239
- output$map <- renderLeaflet({ leaflet() %>% addTiles() })
240
- output$summary <- renderTable({
241
- data.frame(エラー = LOAD_ERROR, stringsAsFactors = FALSE)
242
- })
243
- return()
244
- }
245
-
246
- contract <- DATA$contract
247
- guard <- DATA$guard
248
- avail <- DATA$avail
249
 
250
- # 選択期間のデータを取得
251
- daily <- reactive({
252
- date_range <- input$date_range
253
- d_start <- date_range[1]
254
- d_end <- date_range[2]
255
-
256
- # 期間内に稼働中の現場
257
- active_sites <- contract %>%
258
- filter(!is.na(`案件予定日(開始)`), !is.na(`案件予定日(終了)`)) %>%
259
- filter(`案件予定日(開始)` <= d_end, `案件予定日(終了)` >= d_start)
260
 
261
- # 期間内で「対応可」の日が1日でもある隊員
262
- available_emp <- avail %>%
263
- filter(!is.na(日付), 日付 >= d_start, 日付 <= d_end, available_flag == 1L) %>%
264
- distinct(従業員番号)
265
 
266
- available_guards <- guard %>%
267
- inner_join(available_emp, by = "従業員番号")
 
268
 
269
  list(
270
- date_start = d_start,
271
- date_end = d_end,
272
  active_sites = active_sites,
273
  available_guards = available_guards
274
  )
275
  })
276
 
277
- # 需給サリー
278
  summary_tbl <- reactive({
279
- dd <- daily()
280
- sites <- dd$active_sites
281
- gds <- dd$available_guards
282
 
283
- if (nrow(sites) == 0) {
284
- return(data.frame(メッセージ = "選択期間に稼働中の現場がない", stringsAsFactors = FALSE))
285
- }
286
-
287
- if (input$match_mode == "city") {
288
- g_by_city <- gds %>%
289
- mutate(guard_city = ifelse(is.na(guard_city), "不明", guard_city)) %>%
290
- count(guard_city, name = "available_guards")
291
-
292
- result <- sites %>%
293
- mutate(site_city = ifelse(is.na(site_city), "不明", site_city)) %>%
294
- left_join(g_by_city, by = c("site_city" = "guard_city")) %>%
295
- mutate(
296
- available_guards = ifelse(is.na(available_guards), 0L, available_guards),
297
- shortage = pmax(required_guards - available_guards, 0L),
298
- fulfillment_rate = round(available_guards / required_guards * 100, 0)
299
- )
300
- } else {
301
- radius <- input$radius_km
302
- if (is.null(radius) || is.na(radius) || radius <= 0) radius <- 20
303
-
304
- if (nrow(gds) == 0) {
305
- result <- sites %>%
306
- mutate(
307
- available_guards = 0L,
308
- shortage = required_guards,
309
- fulfillment_rate = 0
310
- )
311
- } else {
312
- result <- sites %>%
313
- rowwise() %>%
314
- mutate(
315
- available_guards = {
316
- dist <- haversine_km(site_lat, site_lng, gds$home_lat, gds$home_lng)
317
- sum(dist <= radius, na.rm = TRUE)
318
- }
319
- ) %>%
320
- ungroup() %>%
321
- mutate(
322
- shortage = pmax(required_guards - available_guards, 0L),
323
- fulfillment_rate = round(available_guards / required_guards * 100, 0)
324
- )
325
- }
326
- }
327
 
328
- # 色と状態を追加
329
- result <- result %>%
330
  mutate(
331
- fill_color = get_fill_color(fulfillment_rate),
332
- status = case_when(
333
- fulfillment_rate < THRESHOLD_LOW ~ "⚠️ 要営業強化",
334
- fulfillment_rate < THRESHOLD_HIGH ~ "△ 注意",
335
- TRUE ~ "○ 充足"
336
- )
337
- )
338
-
339
- result
340
- })
341
-
342
- # 表示用テーブル
343
- output$summary <- renderTable({
344
- tbl <- summary_tbl()
345
- if ("メッセージ" %in% colnames(tbl)) return(tbl)
346
-
347
- tbl %>%
348
- transmute(
349
- 契約ID = contract_id,
350
- 契約No = `契約No`,
351
- 顧客 = `顧客`,
352
- 件名 = paste0(`件名1`, ifelse(is.na(`件名2`) | trimws(`件名2`) == "", "", paste0(" ", `件名2`))),
353
- 市区町村 = site_city,
354
  必要人数 = required_guards,
355
- 対応可能 = as.integer(available_guards),
356
- = paste0(fulfillment_rate, "%"),
357
- 状態 = status
358
  )
359
  })
360
 
361
- # サマリー統計
362
- output$stat_sites <- renderText({
363
- dd <- daily()
364
- paste0(nrow(dd$active_sites), " 件")
365
- })
366
-
367
- output$stat_required <- renderText({
368
- tbl <- summary_tbl()
369
- if ("メッセージ" %in% colnames(tbl)) return("-")
370
- paste0(sum(tbl$required_guards, na.rm = TRUE), " 人")
371
- })
372
-
373
- output$stat_available <- renderText({
374
- dd <- daily()
375
- paste0(nrow(dd$available_guards), " 人")
376
- })
377
-
378
- output$stat_avg_rate <- renderText({
379
- tbl <- summary_tbl()
380
- if ("メッセージ" %in% colnames(tbl)) return("-")
381
- avg_rate <- mean(tbl$fulfillment_rate, na.rm = TRUE)
382
- paste0(round(avg_rate, 0), "%")
383
- })
384
-
385
- # 地図(初回のみ)
386
  output$map <- renderLeaflet({
387
- center_lat <- mean(c(BBOX$lat_min, BBOX$lat_max))
388
- center_lng <- mean(c(BBOX$lng_min, BBOX$lng_max))
389
 
390
- leaflet() %>%
 
391
  addTiles() %>%
392
- setView(lng = center_lng, lat = center_lat, zoom = 9) %>%
393
- addLayersControl(
394
- overlayGroups = c("現場(充足率)", "アベイラブル隊員"),
395
- options = layersControlOptions(collapsed = FALSE)
396
- )
397
- })
398
-
399
- # 地図更新(マーカー)
400
- observe({
401
- dd <- daily()
402
- sites_data <- summary_tbl()
403
- gds <- dd$available_guards
404
-
405
- proxy <- leafletProxy("map")
406
- proxy %>%
407
- clearGroup("現場(充足率)") %>%
408
- clearGroup("アベイラブル隊員")
409
 
410
- # 現場マーカー(充足率付き)
411
- if (!("メッセージ" %in% colnames(sites_data)) && nrow(sites_data) > 0) {
412
- # CircleMarkersを追加
413
- proxy %>%
414
  addCircleMarkers(
415
- data = sites_data,
416
- lng = ~site_lng, lat = ~site_lat,
417
- radius = 14,
418
- color = ~fill_color,
419
- fillColor = ~fill_color,
420
- fillOpacity = 0.85,
421
- weight = 2,
422
- label = ~paste0(
423
- "【", `契約No`, "】", `件名1`,
424
- " | 充足率: ", fulfillment_rate, "%",
425
- " | 必要: ", required_guards, "人",
426
- " | 対応可: ", available_guards, "人"
427
- ),
428
- group = "現場(充足率)"
429
  )
430
-
431
- # 充足率ラベルを個別に追加(色を動的に設定するため)
432
- for (i in seq_len(nrow(sites_data))) {
433
- row <- sites_data[i, ]
434
- proxy %>%
435
- addLabelOnlyMarkers(
436
- lng = row$site_lng,
437
- lat = row$site_lat,
438
- label = paste0(row$fulfillment_rate, "%"),
439
- labelOptions = labelOptions(
440
- noHide = TRUE,
441
- direction = "top",
442
- textOnly = TRUE,
443
- style = list(
444
- "font-weight" = "bold",
445
- "font-size" = "11px",
446
- "color" = "white",
447
- "background-color" = row$fill_color,
448
- "padding" = "2px 5px",
449
- "border-radius" = "4px"
450
- )
451
- ),
452
- group = "現場(充足率)"
453
- )
454
- }
455
  }
456
 
457
- # アベイラブル隊マーカー
458
- if (isTRUE(input$show_guards) && nrow(gds) > 0) {
459
- proxy %>%
460
  addCircleMarkers(
461
- data = gds,
462
- lng = ~home_lng, lat = ~home_lat,
463
- radius = 6,
464
- color = "#3498db",
465
- fillColor = "#3498db",
466
- fillOpacity = 0.6,
467
- weight = 1,
468
- label = ~paste0("従業員番号: ", 従業員番号, " | ", 苗字, " ", 名前, " | ", guard_city),
469
- group = "アベイラブル隊員"
470
  )
471
  }
 
 
 
 
 
 
 
 
 
 
472
  })
473
  }
474
 
475
- shinyApp(ui, server)
 
1
+ # app.R
 
2
 
3
  library(shiny)
4
  library(dplyr)
5
+ # library(lubridate) # 不要なので削除
6
  library(leaflet)
7
 
8
+ set.seed(123)
 
 
 
 
 
 
 
 
9
 
10
+ # =========================
11
+ # 1. ダミーデータ作成
12
+ # =========================
13
 
14
+ # 工事現場(北海道の適当な範囲でランダム)
15
+ sites <- tibble::tibble(
16
+ site_id = 1:5,
17
+ site_name = paste0("工事現場", 1:5),
18
+ city = c("札幌市", "札幌市", "旭川市", "函館市", "帯広市"),
19
+ lat = runif(5, 42.0, 44.5), # 北海道っぽい緯度
20
+ lng = runif(5, 141.0, 143.5), # 北海道っぽい経度
21
+ start = as.Date("2025-12-01"),
22
+ end = as.Date("2025-12-31"),
23
+ required_guards = c(3, 4, 2, 5, 3)
24
+ )
25
 
26
+ cities <- sites$city
 
 
 
 
 
 
 
27
 
28
+ # 警備員(住所は市レベルで、対応可能期間は12月中でバラバラ)
29
+ guards <- tibble::tibble(
30
+ guard_id = 1:20,
31
+ guard_name = paste0("警備員", 1:20),
32
+ city = sample(cities, 20, replace = TRUE),
33
+ lat = runif(20, 42.0, 44.5),
34
+ lng = runif(20, 141.0, 143.5),
35
+ available_from = sample(seq(as.Date("2025-12-01"), as.Date("2025-12-15"), by = "day"),
36
+ 20, replace = TRUE),
37
+ available_to = sample(seq(as.Date("2025-12-16"), as.Date("2025-12-31"), by = "day"),
38
+ 20, replace = TRUE)
39
+ ) %>%
40
+ # from <= to になるように補正
41
+ mutate(
42
+ tmp_min = pmin(available_from, available_to),
43
+ tmp_max = pmax(available_from, available_to),
44
+ available_from = tmp_min,
45
+ available_to = tmp_max
46
+ ) %>%
47
+ select(-tmp_min, -tmp_max)
48
 
49
+ # =========================
50
+ # 2. UI
51
+ # =========================
 
 
 
 
 
 
 
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  ui <- fluidPage(
54
+ titlePanel("北海道・工事現場 × 警備員マッチング(ダミーデータ)"),
 
 
 
 
 
 
 
 
 
55
  sidebarLayout(
56
  sidebarPanel(
57
+ dateInput(
58
+ "selected_date",
59
+ "日付を選択",
60
+ value = as.Date("2025-12-01"),
61
+ min = as.Date("2025-12-01"),
62
+ max = as.Date("2025-12-31")
63
+ ),
64
+ checkboxInput("show_guards", "警備員の位置も地図に表示", TRUE),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  width = 3
66
  ),
67
  mainPanel(
68
+ leafletOutput("map", height = 500),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  br(),
70
+ h4("工事現場別マッチング状況(選択日ベース)"),
71
  tableOutput("summary"),
72
  width = 9
73
  )
74
  )
75
  )
76
 
77
+ # =========================
78
+ # 3. Server
79
+ # =========================
80
+
81
  server <- function(input, output, session) {
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ # 選択日ごとのデータ
84
+ daily_data <- reactive({
85
+ d <- input$selected_date
 
 
 
 
 
 
 
86
 
87
+ # その日に稼働中の現場(期間内
88
+ active_sites <- sites %>%
89
+ filter(start <= d, end >= d)
 
90
 
91
+ # その日に勤務可能な警備員
92
+ available_guards <- guards %>%
93
+ filter(available_from <= d, available_to >= d)
94
 
95
  list(
96
+ date = d,
 
97
  active_sites = active_sites,
98
  available_guards = available_guards
99
  )
100
  })
101
 
102
+ # マッチング集(ここでは「同じ市にいる警備員数」で集計)
103
  summary_tbl <- reactive({
104
+ dd <- daily_data()
105
+ active_sites <- dd$active_sites
106
+ available_guards <- dd$available_guards
107
 
108
+ guards_by_city <- available_guards %>%
109
+ count(city, name = "available_guards")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ active_sites %>%
112
+ left_join(guards_by_city, by = "city") %>%
113
  mutate(
114
+ available_guards = ifelse(is.na(available_guards), 0L, available_guards),
115
+ shortage = pmax(required_guards - available_guards, 0L)
116
+ ) %>%
117
+ select(
118
+ 現場ID = site_id,
119
+ 現場名 = site_name,
120
+ 市区町村 = city,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  必要人数 = required_guards,
122
+ "対応可能人数(同一市内)" = available_guards,
123
+ "不人数" = shortage
 
124
  )
125
  })
126
 
127
+ # 地図描画
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  output$map <- renderLeaflet({
129
+ dd <- daily_data()
 
130
 
131
+ # ベースマップ(北海道あたりを初期表示)
132
+ m <- leaflet() %>%
133
  addTiles() %>%
134
+ setView(lng = 142.0, lat = 43.0, zoom = 6)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ # 工事現場
137
+ if (nrow(dd$active_sites) > 0) {
138
+ m <- m %>%
 
139
  addCircleMarkers(
140
+ data = dd$active_sites,
141
+ ~lng, ~lat,
142
+ radius = 8,
143
+ color = "blue",
144
+ fillOpacity = 0.8,
145
+ label = ~paste0(site_name, "(必要人数: ", required_guards, "人)"),
146
+ group = "sites"
 
 
 
 
 
 
 
147
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
+ # 警備
151
+ if (input$show_guards && nrow(dd$available_guards) > 0) {
152
+ m <- m %>%
153
  addCircleMarkers(
154
+ data = dd$available_guards,
155
+ ~lng, ~lat,
156
+ radius = 5,
157
+ color = "red",
158
+ fillOpacity = 0.7,
159
+ label = ~paste0(guard_name, "(", city, "在住)"),
160
+ group = "guards"
 
 
161
  )
162
  }
163
+
164
+ m %>% addLayersControl(
165
+ overlayGroups = c("sites", "guards"),
166
+ options = layersControlOptions(collapsed = FALSE)
167
+ )
168
+ })
169
+
170
+ # 集計テーブル
171
+ output$summary <- renderTable({
172
+ summary_tbl()
173
  })
174
  }
175
 
176
+ shinyApp(ui, server)