送迎ツールプロトタイプ
休みの管理、振替の追加に成功!
function doGet() {
return HtmlService.createHtmlOutputFromFile('送迎');
}
// サーバー側で取得するデータをクライアント側に渡す
function getLatLngData(slotSelected = "AM", daySelected = "月", dateSelected) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('基本情報');
const data = sheet.getDataRange().getValues();
let locations = [];
data.forEach((row, index) => {
if (index === 0) return; // ヘッダー行をスキップ
let name = row[0]; // 利用者名
let slot = row[1].trim(); // 時間帯 (AM/PM)
let days = row[2].split(",").map(day => day.trim()); // 曜日
let address = row[3]; // 住所
let lat = row[4]; // 緯度
let lng = row[5]; // 経度
let date = row[6]; // 日付
// 緯度経度がない場合、Google Geocoding APIで住所から取得
if (!lat || !lng) {
const geoData = getGeocodeFromAddress(address);
lat = geoData.lat;
lng = geoData.lng;
sheet.getRange(index + 1, 5).setValue(lat); // E列に緯度
sheet.getRange(index + 1, 6).setValue(lng); // F列に経度
}
// 時間帯・曜日・日付でフィルタリング
if (slot === slotSelected && days.includes(daySelected) && (!dateSelected || date === dateSelected)) {
locations.push({
lat: lat,
lng: lng,
name: name,
address: address
});
}
});
return locations; // マップ用の位置情報を返す
}
function getGeocodeFromAddress(address) {
const apiKey = 'AIzaSyBk1uExHU8ebX-fqOMnnxyAWSAlaAZRxHg';
const url = https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}
;
const response = UrlFetchApp.fetch(url);
const json = JSON.parse(response.getContentText());
if (json.status === 'OK') {
const location = json.results[0].geometry.location;
return { lat: location.lat, lng: location.lng };
} else {
throw new Error('Geocoding API Error: ' + json.status);
}
}
function getAllUsers() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('基本情報');
const data = sheet.getDataRange().getValues();
let users = [];
data.forEach((row, index) => {
if (index === 0) return;
let name = row[0];
let lat = row[4];
let lng = row[5];
users.push({
name: name,
lat: lat,
lng: lng
});
});
return users;
}
<!DOCTYPE html>
<html>
<head>
<title>送迎ルートマップ</title>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBk1uExHU8ebX-fqOMnnxyAWSAlaAZRxHg" async defer></script>
<script>
var map;
var directionsService;
var directionsRenderer;
var markers = [];
var excludedMarkers = [];
var streetViewPanorama;
var car1Markers = [];
var car2Markers = [];
var car3Markers = [];
var routeText = ""; // ルートのテキストを保存
function initMap() {
const mapOptions = {
zoom: 10,
center: { lat: 35.6885, lng: 139.7454 }, // イオンスマイル市川の位置
};
map = new google.maps.Map(document.getElementById("map"), mapOptions);
streetViewPanorama = map.getStreetView(); // ストリートビューの初期化
directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(map);
loadLocations();
loadAllUsers();
}
function loadLocations() {
var slot = document.getElementById("slot").value;
var day = document.getElementById("day").value;
var date = document.getElementById("date").value;
google.script.run.withSuccessHandler(function(locations) {
markers.forEach(marker => marker.setMap(null)); // 既存のマーカーを削除
markers = [];
excludedMarkers = [];
locations.forEach(function(location) {
var marker = new google.maps.Marker({
position: { lat: parseFloat(location.lat), lng: parseFloat(location.lng) },
map: map,
title: location.name + " - 順番: " + location.order, // 順番情報を表示
});
markers.push(marker);
google.maps.event.addListener(marker, 'click', function() {
showCustomDialog(marker);
});
});
}).getLatLngData(slot, day, date);
}
function loadAllUsers() {
google.script.run.withSuccessHandler(function(users) {
var select = document.getElementById("userSelect");
select.innerHTML = '';
users.forEach(function(user) {
var option = document.createElement("option");
option.value = JSON.stringify(user);
option.text = user.name;
select.add(option);
});
}).getAllUsers();
}
function addUser() {
var selectedUser = JSON.parse(document.getElementById("userSelect").value);
var marker = new google.maps.Marker({
position: { lat: parseFloat(selectedUser.lat), lng: parseFloat(selectedUser.lng) },
map: map,
title: selectedUser.name,
});
markers.push(marker);
// 追加後のマーカークリックイベントを追加
google.maps.event.addListener(marker, 'click', function() {
showCustomDialog(marker);
});
}
function showCustomDialog(marker) {
var dialog = document.createElement("div");
dialog.className = "custom-dialog";
var text = document.createElement("p");
text.textContent = marker.title + " をどうしますか?";
dialog.appendChild(text);
var addToCar1 = document.createElement("button");
addToCar1.textContent = "1号車に追加";
addToCar1.onclick = function() {
addToCarTable(1, marker.title);
car1Markers.push(marker);
dialog.remove();
};
dialog.appendChild(addToCar1);
var addToCar2 = document.createElement("button");
addToCar2.textContent = "2号車に追加";
addToCar2.onclick = function() {
addToCarTable(2, marker.title);
car2Markers.push(marker);
dialog.remove();
};
dialog.appendChild(addToCar2);
var addToCar3 = document.createElement("button");
addToCar3.textContent = "3号車に追加";
addToCar3.onclick = function() {
addToCarTable(3, marker.title);
car3Markers.push(marker);
dialog.remove();
};
dialog.appendChild(addToCar3);
var excludeButton = document.createElement("button");
excludeButton.textContent = "欠席として除外";
excludeButton.onclick = function() {
excludedMarkers.push(marker);
marker.setMap(null);
dialog.remove();
};
dialog.appendChild(excludeButton);
var streetViewButton = document.createElement("button");
streetViewButton.textContent = "ストリートビュー";
streetViewButton.onclick = function() {
showStreetView(marker.getPosition());
dialog.remove();
};
dialog.appendChild(streetViewButton);
var cancelButton = document.createElement("button");
cancelButton.textContent = "キャンセル";
cancelButton.onclick = function() {
dialog.remove();
};
dialog.appendChild(cancelButton);
document.body.appendChild(dialog);
}
function addToCarTable(carNumber, name) {
var tableId = 'car' + carNumber + 'Table';
var table = document.getElementById(tableId);
var row = table.insertRow();
var cell1 = row.insertCell(0);
var cell2 = row.insertCell(1);
var cell3 = row.insertCell(2);
var cell4 = row.insertCell(3); // 修正・削除用のセルを追加
var cell5 = row.insertCell(4); // 順番変更用のセルを追加
cell1.innerHTML = name;
cell2.innerHTML = carNumber + '号車';
cell3.innerHTML = (name === "イオンスマイル市川店") ? "" : "時間を計算中...";
// 削除ボタン
var deleteButton = document.createElement("button");
deleteButton.textContent = "削除";
deleteButton.onclick = function() {
table.deleteRow(row.rowIndex);
markers = markers.filter(marker => marker.title !== name); // マーカーを削除
};
cell4.appendChild(deleteButton);
// 順番変更ボタン
var moveUpButton = document.createElement("button");
moveUpButton.textContent = "↑";
moveUpButton.onclick = function() {
if (row.previousElementSibling) {
row.parentNode.insertBefore(row, row.previousElementSibling);
}
};
cell5.appendChild(moveUpButton);
var moveDownButton = document.createElement("button");
moveDownButton.textContent = "↓";
moveDownButton.onclick = function() {
if (row.nextElementSibling) {
row.parentNode.insertBefore(row.nextElementSibling, row);
}
};
cell5.appendChild(moveDownButton);
}
function showStreetView(position) {
var panorama = new google.maps.StreetViewPanorama(document.getElementById("streetView"), {
position: position,
pov: {
heading: 34,
pitch: 10
},
zoom: 1
});
streetViewPanorama.setPano(panorama);
}
function backToMapView() {
streetViewPanorama.setVisible(false);
}
// ルート案内と時間計算
function calculateAndDisplayRoute(carNumber) {
var carMarkers;
if (carNumber === 1) {
carMarkers = car1Markers;
} else if (carNumber === 2) {
carMarkers = car2Markers;
} else {
carMarkers = car3Markers;
}
if (carMarkers.length > 0) {
directionsRenderer.setMap(null); // 前回のルート案内をリセット
directionsRenderer = new google.maps.DirectionsRenderer(); // 新しいレンダラーを作成
directionsRenderer.setMap(map);
const waypoints = carMarkers.map(marker => ({
location: marker.getPosition(),
stopover: true
}));
const origin = carMarkers[0].getPosition();
const destination = carMarkers[carMarkers.length - 1].getPosition();
directionsService.route({
origin: origin,
destination: destination,
waypoints: waypoints.slice(1, waypoints.length - 1),
optimizeWaypoints: true,
travelMode: 'DRIVING'
}, function(response, status) {
if (status === 'OK') {
directionsRenderer.setDirections(response);
const route = response.routes[0];
var legTimes = []; // 各区間の所要時間を保存
// 各区間の所要時間を計算してテーブルに反映
route.legs.forEach(function(leg, index) {
var timeText = `${Math.round(leg.duration.value / 60)} 分`;
legTimes.push(timeText);
var table = document.getElementById(`car${carNumber}Table`);
if (table.rows[index + 1].cells[0].innerText !== "イオンスマイル市川店") {
table.rows[index + 1].cells[2].innerText = timeText; // 時間を更新
}
});
// ルート案内の詳細テキストを生成
routeText = "";
route.legs.forEach(function(leg, index) {
routeText += `ステップ ${index + 1}: ${leg.start_address} から ${leg.end_address} まで: ${leg.duration.text}\n`;
});
document.getElementById("routeDetails").innerText = routeText;
} else {
window.alert('Directions request failed due to ' + status);
}
});
}
}
// ルートのテキスト表示をトグル
function toggleRouteDetails() {
var routeDetailsDiv = document.getElementById("routeDetailsDiv");
if (routeDetailsDiv.style.display === "none") {
routeDetailsDiv.style.display = "block";
} else {
routeDetailsDiv.style.display = "none";
}
}
</script>
<style>
#map, #streetView {
width: 48%;
height: 500px;
float: left;
}
.car-tables {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
table {
margin-right: 20px;
border-collapse: collapse;
}
table, th, td {
border: 1px solid black;
padding: 10px;
}
.custom-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border: 1px solid black;
padding: 20px;
z-index: 1000;
}
.custom-dialog button {
margin-right: 10px;
}
/* フォントサイズをさらに大きく調整 */
.input-labels {
font-size: 1.5em;
margin-right: 10px;
}
select, input[type="date"] {
font-size: 1.5em;
padding: 5px;
}
button {
font-size: 1.3em;
padding: 5px 10px;
}
/* ルートテキストのスタイル */
#routeDetailsDiv {
margin-top: 20px;
display: none;
}
/* スマホ向けルートリンク */
#smartphoneRouteLink {
display: none;
font-size: 1.2em;
margin-top: 20px;
}
</style>
</head>
<body onload="initMap()">
<h1>送迎ルートマップ</h1>
<div>
<label for="slot" class="input-labels">時間帯選択: </label>
<select id="slot" onchange="loadLocations()">
<option value="AM">AM</option>
<option value="PM">PM</option>
</select>
<label for="day" class="input-labels">曜日選択: </label>
<select id="day" onchange="loadLocations()">
<option value="月">月</option>
<option value="火">火</option>
<option value="水">水</option>
<option value="木">木</option>
<option value="金">金</option>
<option value="土">土</option>
<option value="日">日</option>
</select>
<label for="date" class="input-labels">日付選択: </label>
<input type="date" id="date" onchange="loadLocations()">
<label for="userSelect" class="input-labels">振替利用者を追加: </label>
<select id="userSelect"></select>
<button onclick="addUser()">追加</button>
</div>
<div class="car-tables">
<div>
<h2>1号車</h2>
<table id="car1Table" border="1">
<tr>
<th>名前</th>
<th>車両番号</th>
<th>時間</th>
<th>操作</th>
<th>順番変更</th> <!-- 順番変更ボタンを追加 -->
</tr>
</table>
<button onclick="calculateAndDisplayRoute(1)">1号車のルート案内を開始</button>
</div>
<div>
<h2>2号車</h2>
<table id="car2Table" border="1">
<tr>
<th>名前</th>
<th>車両番号</th>
<th>時間</th>
<th>操作</th>
<th>順番変更</th>
</tr>
</table>
<button onclick="calculateAndDisplayRoute(2)">2号車のルート案内を開始</button>
</div>
<div>
<h2>3号車</h2>
<table id="car3Table" border="1">
<tr>
<th>名前</th>
<th>車両番号</th>
<th>時間</th>
<th>操作</th>
<th>順番変更</th>
</tr>
</table>
<button onclick="calculateAndDisplayRoute(3)">3号車のルート案内を開始</button>
</div>
</div>
<div id="map"></div>
<div id="streetView"></div>
<div>
<button onclick="toggleRouteDetails()">ルートの詳細を表示/非表示</button>
<div id="routeDetailsDiv">
<pre id="routeDetails"></pre>
</div>
</div>
</body>
</html>
10月22日時点のコード
スマホで展開できればゴール