yarnom 7555c4c72b fix: 天气预测接口更替为开源的 Open-Meteo
- 默认情况下,将结合最适合的天气模型(auto)
- 预报 3天长度
2025-07-08 12:15:54 +08:00

1032 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--由于需要一些特定的页面效果,
导致无法使用layui提供的样式所以这里我还是自己写css页面主要的 js 在 static/js/device-overview 库中它是通过暴露方法让main JS 获得方法的-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>设备总览</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="../lib/layui-v2.6.3/css/layui.css" media="all">
<link rel="stylesheet" href="../lib/font-awesome-4.7.0/css/font-awesome.min.css" media="all">
<link rel="stylesheet" href="../css/public.css" media="all">
<link rel="stylesheet" href="../css/ol.css">
<!-- 谷歌地图的API接口这个访问没有天地图的次数多所以这边限制只有管理员能使用 -->
<!-- 账户是 fengyarnom@gmail.com-->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBEBxB5n9vikPHbfNMzo_dzneXJi77YqjE" async defer></script>
<style>
.top-panel {
border: 1px solid #eceff9;
border-radius: 5px;
text-align: center;
}
.stats-panel {
height: 100%;
display: flex;
flex-direction: column;
}
.stats-panel > .layui-card-body {
height: auto;
min-height: 60px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.top-panel-number {
font-size: 30px;
border-right: 1px solid #eceff9;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.top-panel-tips{
line-height:30px;
font-size: 12px
}
.flex-row {
display: flex;
align-items: stretch;
flex-wrap: wrap;
}
.flex-row > .layui-col-xs12,
.flex-row > .layui-col-md6 {
display: flex;
flex-direction: column;
}
.stats-panel .layui-col-xs3 a,
.stats-panel .layui-col-md4 a,
.stats-panel .layui-col-xs3 div a,
.stats-panel .layui-col-md4 div a {
display: block;
margin-bottom: 2px;
}
ul li {
list-style: none;
}
.layuimini-main {
height: 96vh;
display: flex;
flex-direction: column;
}
.map-wrapper {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.map-card {
flex: 1;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #e6e8eb;
display: flex;
flex-direction: column;
overflow: hidden;
}
#map-container {
flex: 1;
width: 100%;
padding: 0;
margin: 0;
position: relative;
}
.device-overview-bar {
background: white;
border: 1px solid #e6e8eb;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
padding: 10px 15px;
margin-bottom: 10px;
color: #333;
}
.map-toolbar {
position: absolute;
top: 15px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 8px 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 12px;
width:auto;
max-width: 95vw;
}
.toolbar-item {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.toolbar-label {
font-size: 12px;
color: #666;
white-space: nowrap;
font-weight: 500;
}
.toolbar-select {
border: 1px solid rgba(230, 230, 230, 0.8);
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
background: rgba(255, 255, 255, 0.9);
min-width: 120px;
height: 28px;
}
.toolbar-input {
border: 1px solid rgba(230, 230, 230, 0.8);
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
background: rgba(255, 255, 255, 0.9);
width: 140px;
height: 28px;
}
.toolbar-btn {
background: rgba(26, 160, 148, 0.9);
color: white;
border: none;
border-radius: 6px;
padding: 4px 12px;
font-size: 12px;
cursor: pointer;
height: 28px;
transition: all 0.2s ease;
}
.toolbar-btn:hover {
background: rgba(26, 160, 148, 1);
transform: translateY(-1px);
}
.toolbar-divider {
width: 1px;
height: 20px;
background: rgba(230, 230, 230, 0.6);
}
.overview-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.stat-item {
display: flex;
align-items: center;
/* background: #f8f9fa;
border: 1px solid #e9ecef; */
border-radius: 4px;
padding: 5px 10px;
min-width: 85px;
transition: background-color 0.2s ease;
cursor: pointer;
height: 28px;
}
.stat-item:hover {
background: #e9ecef;
}
.stat-item.non-clickable {
cursor: default;
}
.stat-item.non-clickable:hover {
background: #f8f9fa;
}
.stat-dot {
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 6px;
flex-shrink: 0;
}
.dot-green { background-color: #48bb78; }
.dot-blue { background-color: #4299e1; }
.dot-gray { background-color: #a0aec0; }
.dot-orange { background-color: #ed8936; }
.dot-red { background-color: #f56565; }
.dot-yellow { background-color: #ecc94b; }
.stat-item span:nth-child(2) {
font-size: 12px;
color: #666;
margin-right: 4px;
white-space: nowrap;
}
.stat-number {
font-size: 13px;
font-weight: bold;
color: #333;
}
/* 由于不使用layui的元素所以这边还得适配手机端 */
@media screen and (max-width: 768px) {
.layuimini-main {
height: 120vh;
overflow: hidden;
}
.map-wrapper {
flex: 1;
overflow: hidden;
}
.map-card {
margin: 8px;
height: calc(100% - 16px);
overflow: hidden;
}
#map-container {
height: 100%;
overflow: hidden;
}
.device-overview-bar {
padding: 8px 12px;
margin-bottom: 8px;
flex-shrink: 0;
}
.overview-stats {
gap: 6px;
flex-wrap: wrap;
justify-content: flex-start;
}
.stat-item {
flex: 0 0 auto;
min-width: 70px;
width: auto;
max-width: calc(50% - 4px);
padding: 4px 6px;
font-size: 10px;
height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 2px;
}
.stat-item span:not(.stat-dot):not(.stat-number) {
font-size: 9px;
max-width: 40px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stat-dot {
width: 6px;
height: 6px;
margin-right: 0;
flex-shrink: 0;
}
.stat-number {
font-size: 12px;
font-weight: bold;
color: #333;
}
.map-toolbar {
position: absolute;
transform: none;
left: auto;
margin: 8px;
margin-bottom: 8px;
min-width: auto;
max-width: none;
flex-wrap: wrap;
gap: 6px;
padding: 6px 10px;
top:0px;
}
.toolbar-item {
gap: 4px;
flex: 0 0 auto;
width:100%
}
.toolbar-label {
font-size: 10px;
display: none;
}
.toolbar-select,
.toolbar-input {
font-size: 11px;
height: 24px;
width: 100%;
}
.toolbar-btn {
font-size: 11px;
height: 24px;
padding: 2px 8px;
}
.toolbar-divider {
display: none;
}
#toolbar-device-locate span:not(.toolbar-label) {
font-size: 10px;
padding: 2px 4px;
}
.dropdown-menu {
min-width: 180px;
max-width: 95vw;
}
.dropdown-item {
padding: 8px 12px;
font-size: 12px;
}
}
.ol-tooltip {
position: absolute;
background: rgba(255,255,255,0.8);
border-radius: 4px;
padding: 4px 8px;
border: 1px solid #d9d9d9;
color: #333;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
z-index: 1001;
}
.ol-tooltip-static {
background: #ffcc33;
color: #222;
border: 1px solid #ffcc33;
}
.dropdown-container {
position: relative;
display: inline-block;
width: 100%;
}
.dropdown-toggle {
cursor: pointer;
display: flex;
align-items: center;
width: 100%;
justify-content: center;
}
.dropdown-toggle::after {
content: '';
display: inline-block;
margin-left: 6px;
border-top: 4px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
vertical-align: middle;
}
.dropdown-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
z-index: 1100;
min-width: 200px;
padding: 8px 0;
margin-top: 2px;
background: white;
border-radius: 6px;
box-shadow: 0 3px 12px rgba(0,0,0,0.15);
border: 1px solid rgba(0,0,0,0.1);
}
.dropdown-menu.show {
display: block;
}
.dropdown-group {
padding: 0 8px;
}
.dropdown-group-title {
font-size: 12px;
color: #666;
padding: 6px 8px;
font-weight: 500;
}
.dropdown-item {
padding: 6px 12px;
cursor: pointer;
font-size: 13px;
color: #333;
border-radius: 4px;
margin: 2px 0;
}
.dropdown-item:hover {
background: rgba(26, 160, 148, 0.1);
}
.dropdown-divider {
height: 1px;
background: #e9ecef;
margin: 8px 0;
}
.switch-label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
cursor: pointer;
}
.switch-label input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.switch-slider {
position: relative;
display: inline-block;
width: 36px;
height: 18px;
background-color: #ccc;
border-radius: 18px;
transition: .4s;
flex-shrink: 0;
}
.switch-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
transition: .4s;
}
input:checked + .switch-slider {
background-color: #1aa094;
}
input:checked + .switch-slider:before {
transform: translateX(18px);
}
/* 测量状态指示器样式 */
#measureStatus {
background: rgba(255, 255, 255, 0.98);
border: 1px solid rgba(72, 187, 120, 0.3);
border-radius: 8px;
padding: 6px 12px;
box-shadow: 0 2px 8px rgba(72, 187, 120, 0.2);
align-items: center;
gap: 8px;
}
#measureStatus .toolbar-btn {
padding: 2px 6px;
height: 24px;
min-width: 24px;
}
.weather-forecast-card {
position: absolute;
top: 55%;
right: 20px;
transform: translateY(-50%);
width: 280px;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
z-index: 1100;
animation: slideInRight 0.3s ease-out;
padding: 16px;
}
@keyframes slideInRight {
from {
transform: translate(100%, -50%);
opacity: 0;
}
to {
transform: translate(0, -50%);
opacity: 1;
}
}
.weather-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 10px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 8px 8px 0 0;
transition: background-color 0.2s ease;
margin: -16px -16px 12px -16px;
}
.weather-card-header:hover {
background-color: rgba(26, 160, 148, 0.05);
}
.weather-card-title {
font-size: 16px;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.weather-close-btn {
background: none;
border: none;
color: #718096;
cursor: pointer;
font-size: 18px;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
line-height: 1;
}
.weather-close-btn:hover {
background: rgba(116, 129, 141, 0.1);
color: #4a5568;
}
.weather-device-info {
background: rgba(26, 160, 148, 0.1);
border-radius: 8px;
padding: 10px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.weather-device-info span {
font-size: 12px;
color: #4a5568;
}
.weather-forecast-navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
background: #f7fafc;
border-radius: 8px;
padding: 6px 10px;
}
.weather-nav-btn {
background: rgba(26, 160, 148, 0.1);
border: none;
color: #1aa094;
border-radius: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.weather-nav-btn:hover:not(:disabled) {
background: rgba(26, 160, 148, 0.2);
}
.weather-nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.forecast-time-display {
font-size: 13px;
font-weight: 600;
color: #2d3748;
}
.weather-card-content {
padding: 12px;
background: #f7fafc;
border-radius: 8px;
}
.weather-loading {
text-align: center;
padding: 20px;
color: #718096;
}
.weather-error {
text-align: center;
padding: 20px;
color: #e53e3e;
background: rgba(229, 62, 62, 0.1);
border-radius: 8px;
margin: 10px 0;
}
.weather-forecast-item {
border-radius: 6px;
background: rgba(247, 250, 252, 0.5);
}
.weather-param-grid {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
}
.weather-param {
display: flex;
align-items: center;
gap: 4px;
}
.weather-param-icon {
font-size: 10px;
color: #1aa094;
width: 14px;
text-align: center;
}
.weather-param-label {
font-size: 10px;
color: #718096;
font-weight: 500;
flex: 1;
}
.weather-param-value {
font-size: 10px;
color: #2d3748;
font-weight: 600;
}
.weather-wind-direction {
display: inline-block;
transform-origin: center;
transition: transform 0.3s ease;
}
/* 手机端适配 */
@media screen and (max-width: 768px) {
.weather-forecast-card {
right: 10px;
width: 240px;
padding: 12px;
max-height: 80vh;
overflow-y: auto;
}
.weather-card-header {
margin-bottom: 8px;
}
.weather-card-title {
font-size: 14px;
}
.weather-device-info {
padding: 8px;
margin-bottom: 8px;
gap: 4px;
}
.weather-device-info span {
font-size: 10px;
}
.weather-forecast-navigation {
padding: 4px 8px;
margin-bottom: 8px;
}
.weather-nav-btn {
width: 24px;
height: 24px;
}
.forecast-time-display {
font-size: 11px;
}
.weather-card-content {
padding: 8px;
}
.weather-param {
gap: 2px;
}
.weather-param-icon {
font-size: 8px;
width: 12px;
}
.weather-param-label,
.weather-param-value {
font-size: 8px;
}
#measureStatus{
display: flex;
justify-content: center;
padding: 6px 0px;
}
}
</style>
<script src="../js/ol.js"></script>
</head>
<body>
<div class="layuimini-main">
<div class="device-overview-bar">
<div class="overview-stats">
<div class="stat-item non-clickable">
<span class="stat-dot dot-green"></span>
<span>设备数量</span>
<span class="stat-number" th:text="${deviceOnlineNum}">1101</span>
</div>
<div class="stat-item non-clickable">
<span class="stat-dot dot-blue"></span>
<span>装机量</span>
<span class="stat-number" th:text="${deviceDeployedNum}">1123</span>
</div>
<div class="stat-item" onclick="queryDevices('offline')" th:if="${deviceOfflineNum > 0}">
<span class="stat-dot dot-gray"></span>
<span>掉线数</span>
<span class="stat-number" th:text="${deviceOfflineNum}">22</span>
</div>
<div class="stat-item" onclick="queryDevices('no_fwd')" th:if="${deviceNoFwdNum > 0}">
<span class="stat-dot dot-orange"></span>
<span>未推送数</span>
<span class="stat-number" th:text="${deviceNoFwdNum}">11</span>
</div>
<div class="stat-item" onclick="queryDevices('nofixed')" th:if="${noFix > 0}">
<span class="stat-dot dot-red"></span>
<span>长期无效解</span>
<span class="stat-number" th:text="${noFix}">10</span>
</div>
<div class="stat-item" onclick="queryDevices('warning2')" th:if="${warning2Num > 0}">
<span class="stat-dot dot-red"></span>
<span>严重告警</span>
<span class="stat-number" th:text="${warning2Num}">40</span>
</div>
<div class="stat-item" onclick="queryDevices('warning1')" th:if="${warning1Num > 0}">
<span class="stat-dot dot-orange"></span>
<span>一般告警</span>
<span class="stat-number" th:text="${warning1Num}">79</span>
</div>
<div class="stat-item" onclick="queryDevices('nogga')" th:if="${noGGA > 0}">
<span class="stat-dot dot-gray"></span>
<span>无GGA告警</span>
<span class="stat-number" th:text="${noGGA}">4</span>
</div>
</div>
</div>
<div class="map-wrapper">
<div class="map-card">
<div id="map-container">
<div id="weatherForecastCard" class="weather-forecast-card" style="display: none;" th:if="${role=='SUPER_ADMIN'}">
<div class="weather-card-header">
<h3 class="weather-card-title">
Open-Meteo 天气预测
</h3>
<button class="weather-close-btn" onclick="closeWeatherCard()">×</button>
</div>
<div id="weatherDeviceInfo" class="weather-device-info">
<span id="weatherDeviceId">设备信息</span>
<span id="weatherDeviceCoords">坐标信息</span>
</div>
<div class="weather-forecast-navigation">
<button id="prevForecast" class="weather-nav-btn" onclick="showPrevForecast()" disabled>
<i class="layui-icon layui-icon-left"></i>
</button>
<span id="forecastTimeDisplay" class="forecast-time-display">--:--</span>
<button id="nextForecast" class="weather-nav-btn" onclick="showNextForecast()">
<i class="layui-icon layui-icon-right"></i>
</button>
</div>
<div id="weatherForecastContent" class="weather-card-content">
<div class="weather-loading">
<i class="layui-icon layui-icon-loading layui-icon-anim-rotate"></i>
<p>正在获取天气预测数据...</p>
</div>
</div>
</div>
<div class="map-toolbar">
<div class="toolbar-item">
<select id="mapTypeSelectNew" class="toolbar-select" lay-filter="mapTypeNew" onchange="onMapTypeChange()">
<option value="tianditu_satellite" selected>天地图-卫星影像</option>
<option value="tianditu_terrain_hybrid" >天地图-地形混合</option>
<option value="tianditu_normal">天地图-矢量</option>
<option value="tianditu_terrain">天地图-地形</option>
<option value="amap_satellite">高德-卫星影像</option>
<option value="amap">高德-矢量</option>
<!-- 只能超级管理员使用 -->
<option value="google_satellite" th:if="${role=='SUPER_ADMIN'}">谷歌-卫星影像</option>
<option value="google_terrain" th:if="${role=='SUPER_ADMIN'}">谷歌-地形图</option>
<option value="google_roadmap" th:if="${role=='SUPER_ADMIN'}">谷歌-道路图</option>
<option value="google_hybrid" th:if="${role=='SUPER_ADMIN'}">谷歌-混合</option>
</select>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-item">
<input type="text" id="deviceSearchNew" class="toolbar-input" placeholder="搜索设备编号">
<button class="toolbar-btn" onclick="searchDeviceNew()">搜索</button>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-item">
<select id="warningFilter" class="toolbar-select" onchange="onWarningFilterChange()">
<option value="all">全部设备</option>
<option value="warning1">一般告警</option>
<option value="warning2" selected>严重告警</option>
</select>
</div>
<div class="toolbar-divider"></div>
<div class="toolbar-item">
<div class="dropdown-container">
<button class="toolbar-btn dropdown-toggle" onclick="toggleMapFunctionsMenu()">地图功能</button>
<div id="mapFunctionsMenu" class="dropdown-menu">
<div class="dropdown-group">
<div class="dropdown-group-title">信息显示</div>
<div class="dropdown-item">
<label class="switch-label">
<span>显示设备信息</span>
<input type="checkbox" id="showDeviceIdSwitch" checked onchange="toggleDeviceId()">
<span class="switch-slider"></span>
</label>
</div>
<div class="dropdown-item">
<label class="switch-label">
<span>显示集群</span>
<input type="checkbox" id="showClusterSwitch" checked onchange="toggleCluster()">
<span class="switch-slider"></span>
</label>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-group">
<div class="dropdown-group-title">定位工具</div>
<div class="dropdown-item" onclick="toggleMeasureDistance()">
测距
</div>
<div class="dropdown-item" onclick="clearMeasure()">
清除测距
</div>
<div class="dropdown-item" onclick="locateMyLocation()">
当前位置
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-group" th:if="${role=='SUPER_ADMIN'}">
<div class="dropdown-group-title">Open-Meteo 天气预测</div>
<div class="dropdown-item">
<label class="switch-label">
<span>启用天气预测</span>
<input type="checkbox" id="enableWeatherSwitch" onchange="toggleWeatherForecast()">
<span class="switch-slider"></span>
</label>
</div>
</div>
</div>
</div>
</div>
<div id="measureStatus" class="toolbar-item" style="display: none;">
<span style="color: #48bb78; font-weight: bold; font-size: 12px;">测量中</span>
<button class="toolbar-btn" onclick="finishMeasuring()" style="background: #f56565; margin-left: 6px;">
<i class="layui-icon layui-icon-close" style="font-size: 12px;"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="../js/ol.js"></script>
<script src="../lib/jquery-3.4.1/jquery-3.4.1.min.js" charset="utf-8"></script>
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script src="../js/device-overview-module/coordinate-utils.js"></script>
<script src="../js/device-overview-module/map-layers.js"></script>
<script src="../js/device-overview-module/device-markers.js"></script>
<script src="../js/device-overview-module/search-filter.js"></script>
<script src="../js/device-overview-module/measure-tools.js"></script>
<script src="../js/device-overview-module/weather-forecast.js"></script>
<script src="../js/device-overview-module/map-core.js"></script>
<script src="../js/device-overview-module/device-overview-main.js"></script>
<script th:inline="javascript">
// 从服务器获取设备列表数据
var deviceList = [
/*[# th:each="device : ${deviceList}"]*/
{
deviceid: /*[[${device.deviceid}]]*/ '',
latitude: /*[[${device.latitude}]]*/ 0,
longitude: /*[[${device.longitude}]]*/ 0,
warning: /*[[${device.warning}]]*/ 0
}/*[# th:if="${!deviceStat.last}"]*/,/*[/]*/
/*[/]*/
];
var userRole = /*[[${role}]]*/ 'USER';
DeviceOverview.init(deviceList, {
role: userRole
});
window.onMapTypeChange = DeviceOverview.onMapTypeChange;
window.searchDeviceNew = DeviceOverview.searchDeviceNew;
window.onWarningFilterChange = DeviceOverview.onWarningFilterChange;
window.toggleMapFunctionsMenu = DeviceOverview.toggleMapFunctionsMenu;
window.toggleDeviceId = DeviceOverview.toggleDeviceId;
window.toggleCluster = DeviceOverview.toggleCluster;
window.toggleMeasureDistance = DeviceOverview.toggleMeasureDistance;
window.finishMeasuring = DeviceOverview.finishMeasuring;
window.clearMeasure = DeviceOverview.clearMeasure;
window.toggleWeatherForecast = DeviceOverview.toggleWeatherForecast;
window.closeWeatherCard = DeviceOverview.closeWeatherCard;
window.showPrevForecast = DeviceOverview.showPrevForecast;
window.showNextForecast = DeviceOverview.showNextForecast;
window.queryDevices = DeviceOverview.queryDevices;
window.locateDeviceOnMap = DeviceOverview.locateDeviceOnMap;
window.locateDeviceDirectly = DeviceOverview.locateDeviceDirectly;
window.switchMapType = DeviceOverview.switchMapType;
</script>
</body>
</html>