6497 lines
277 KiB
Python
6497 lines
277 KiB
Python
from __future__ import annotations
|
|
import io
|
|
import math
|
|
import time
|
|
import base64
|
|
import json
|
|
import re
|
|
import zlib
|
|
import logging
|
|
import traceback
|
|
import copy
|
|
import numpy as np
|
|
import hashlib
|
|
from py_mini_racer import MiniRacer
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
from PIL import Image, ImageDraw, ImageOps, ImageFont, ImageEnhance, PngImagePlugin, ImageFilter
|
|
from typing import Any
|
|
from time import sleep
|
|
from io import BytesIO
|
|
from typing import Optional, Tuple
|
|
from functools import cmp_to_key
|
|
from threading import Timer
|
|
from .resources import *
|
|
from .protocol import DreameVacuumProtocol
|
|
from .exceptions import DeviceUpdateFailedException
|
|
from .types import (
|
|
PIID,
|
|
DIID,
|
|
DreameVacuumProperty,
|
|
DreameVacuumAction,
|
|
DreameVacuumActionMapping,
|
|
ObstacleType,
|
|
PathType,
|
|
Point,
|
|
Obstacle,
|
|
MapDataPartial,
|
|
MapData,
|
|
MapFrameType,
|
|
MapPixelType,
|
|
Path,
|
|
Area,
|
|
Wall,
|
|
Segment,
|
|
MapImageDimensions,
|
|
MapRendererLayer,
|
|
MapRendererColorScheme,
|
|
MapRendererConfig,
|
|
MAP_COLOR_SCHEME_LIST,
|
|
MAP_ICON_SET_LIST,
|
|
ALine,
|
|
CLine,
|
|
Paths,
|
|
Angle,
|
|
)
|
|
from .const import (
|
|
MAP_PARAMETER_NAME,
|
|
MAP_PARAMETER_VALUE,
|
|
MAP_PARAMETER_TIME,
|
|
MAP_PARAMETER_CODE,
|
|
MAP_PARAMETER_OUT,
|
|
MAP_PARAMETER_MAP,
|
|
MAP_PARAMETER_ANGLE,
|
|
MAP_PARAMETER_MAPSTR,
|
|
MAP_PARAMETER_CURR_ID,
|
|
MAP_PARAMETER_VACUUM,
|
|
MAP_PARAMETER_ID,
|
|
MAP_PARAMETER_INFO,
|
|
MAP_PARAMETER_FIRST,
|
|
MAP_PARAMETER_OBJNAME,
|
|
MAP_PARAMETER_RESULT,
|
|
MAP_PARAMETER_URL,
|
|
MAP_PARAMETER_EXPIRES_TIME,
|
|
MAP_PARAMETER_THB,
|
|
MAP_PARAMETER_OBJECT_NAME,
|
|
MAP_PARAMETER_MD5,
|
|
MAP_REQUEST_PARAMETER_MAP_ID,
|
|
MAP_REQUEST_PARAMETER_FRAME_ID,
|
|
MAP_REQUEST_PARAMETER_FRAME_TYPE,
|
|
MAP_REQUEST_PARAMETER_REQ_TYPE,
|
|
MAP_REQUEST_PARAMETER_FORCE_TYPE,
|
|
MAP_REQUEST_PARAMETER_TYPE,
|
|
MAP_REQUEST_PARAMETER_INDEX,
|
|
MAP_REQUEST_PARAMETER_ROOM_ID,
|
|
MAP_DATA_PARAMETER_CLASS,
|
|
MAP_DATA_PARAMETER_SIZE,
|
|
MAP_DATA_PARAMETER_X,
|
|
MAP_DATA_PARAMETER_Y,
|
|
MAP_DATA_PARAMETER_PIXEL_SIZE,
|
|
MAP_DATA_PARAMETER_LAYERS,
|
|
MAP_DATA_PARAMETER_ENTITIES,
|
|
MAP_DATA_PARAMETER_META_DATA,
|
|
MAP_DATA_PARAMETER_VERSION,
|
|
MAP_DATA_PARAMETER_ROTATION,
|
|
MAP_DATA_PARAMETER_TYPE,
|
|
MAP_DATA_PARAMETER_POINTS,
|
|
MAP_DATA_PARAMETER_PIXELS,
|
|
MAP_DATA_PARAMETER_SEGMENT_ID,
|
|
MAP_DATA_PARAMETER_ACTIVE,
|
|
MAP_DATA_PARAMETER_NAME,
|
|
MAP_DATA_PARAMETER_DIMENSIONS,
|
|
MAP_DATA_PARAMETER_MIN,
|
|
MAP_DATA_PARAMETER_MAX,
|
|
MAP_DATA_PARAMETER_MID,
|
|
MAP_DATA_PARAMETER_AVG,
|
|
MAP_DATA_PARAMETER_PIXEL_COUNT,
|
|
MAP_DATA_PARAMETER_COMPRESSED_PIXELS,
|
|
MAP_DATA_PARAMETER_ROBOT_POSITION,
|
|
MAP_DATA_PARAMETER_CHARGER_POSITION,
|
|
MAP_DATA_PARAMETER_NO_MOP_AREA,
|
|
MAP_DATA_PARAMETER_NO_GO_AREA,
|
|
MAP_DATA_PARAMETER_ACTIVE_ZONE,
|
|
MAP_DATA_PARAMETER_VIRTUAL_WALL,
|
|
MAP_DATA_PARAMETER_PATH,
|
|
MAP_DATA_PARAMETER_FLOOR,
|
|
MAP_DATA_PARAMETER_WALL,
|
|
MAP_DATA_PARAMETER_SEGMENT,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
class DreameMapVacuumMapManager:
|
|
def __init__(
|
|
self, _protocol: DreameVacuumProtocol
|
|
) -> None:
|
|
self._map_list_object_name: str = None
|
|
self._map_list_md5: str = None
|
|
self._recovery_map_list_object_name: str = None
|
|
self._update_callback = None
|
|
self._error_callback = None
|
|
self._update_timer: Timer = None
|
|
self._update_running: bool = False
|
|
self._update_interval: float = 10
|
|
self._device_running: bool = False
|
|
self._device_docked: bool = False
|
|
self._available: bool = False
|
|
self._ready: bool = False
|
|
self._connected: bool = True
|
|
self._vslam_map: bool = False
|
|
|
|
self._init_data()
|
|
|
|
self._protocol = _protocol
|
|
self.editor = DreameMapVacuumMapEditor(self)
|
|
self.optimizer = DreameVacuumMapOptimizer()
|
|
|
|
def _init_data(self) -> None:
|
|
self._map_data: MapData = None
|
|
self._current_frame_id: int = None
|
|
self._current_map_id: int = None
|
|
self._current_timestamp_ms: int = None
|
|
self._file_urls: dict[str, str] = {}
|
|
self._saved_map_data: dict[int, MapData] = {}
|
|
self._map_list: list[int] = []
|
|
self._recovery_map_data: dict[int, MapData] = {}
|
|
self._need_map_request: bool = False
|
|
self._need_map_list_request: bool = None
|
|
self._need_recovery_map_list_request: bool = None
|
|
self._map_data_queue: dict[int, MapData] = {}
|
|
self._updated_frame_id: int = None
|
|
self._selected_map_id: int = None
|
|
self._request_queue: dict[str, bool] = {}
|
|
self._latest_map_data_time: int = None
|
|
self._latest_object_name_time: int = None
|
|
self._latest_map_timestamp_ms: int = None
|
|
self._latest_map_id: int = None
|
|
self._last_p_request_map_id: int = None
|
|
self._last_p_request_frame_id: int = None
|
|
self._last_p_request_time: int = None
|
|
self._last_robot_time: int = None
|
|
self._map_request_time: int = None
|
|
self._map_request_count: int = 0
|
|
self._new_map_request_time: int = None
|
|
self._aes_iv: str = None
|
|
|
|
def _request_map_from_cloud(self) -> bool:
|
|
if self._current_timestamp_ms is not None:
|
|
start_time = self._current_timestamp_ms
|
|
request_start_time = int(math.floor(start_time / 1000.0))
|
|
else:
|
|
request_start_time = 0
|
|
if self._latest_object_name_time is not None:
|
|
request_start_time = self._latest_object_name_time
|
|
elif self._map_request_time is not None:
|
|
request_start_time = self._map_request_time
|
|
elif self._last_robot_time is not None:
|
|
request_start_time = int(self._last_robot_time / 1000)
|
|
|
|
if (
|
|
self._latest_map_data_time is None
|
|
or self._latest_map_data_time < request_start_time
|
|
):
|
|
self._latest_map_data_time = request_start_time
|
|
|
|
if (
|
|
self._latest_object_name_time is None
|
|
or self._latest_object_name_time < request_start_time
|
|
):
|
|
self._latest_object_name_time = request_start_time
|
|
|
|
map_data_result = self._protocol.cloud.get_device_property(
|
|
DIID(DreameVacuumProperty.MAP_DATA), 20, self._latest_map_data_time
|
|
)
|
|
|
|
if not self._protocol.cloud.connected:
|
|
if self._connected:
|
|
self._connected = False
|
|
self._map_data_changed()
|
|
return False
|
|
elif not self._connected:
|
|
self._connected = True
|
|
self._map_data_changed()
|
|
|
|
if map_data_result is None:
|
|
_LOGGER.warn("Getting map_data from cloud failed")
|
|
map_data_result = []
|
|
|
|
object_name_result = self._protocol.cloud.get_device_property(
|
|
DIID(DreameVacuumProperty.OBJECT_NAME), 1, self._latest_object_name_time
|
|
)
|
|
if object_name_result is None:
|
|
_LOGGER.warn("Getting object_name from cloud failed")
|
|
object_name_result = []
|
|
|
|
next_frame_id = 1
|
|
|
|
if len(map_data_result):
|
|
self._latest_map_data_time = map_data_result[0][MAP_PARAMETER_TIME] + 1
|
|
|
|
if len(object_name_result):
|
|
self._latest_object_name_time = object_name_result[0][MAP_PARAMETER_TIME] + 1
|
|
|
|
for data in map_data_result:
|
|
value = json.loads(data[MAP_PARAMETER_VALUE])
|
|
pmap = value[0]
|
|
timestamp = None
|
|
if data.get(MAP_PARAMETER_TIME):
|
|
timestamp = data[MAP_PARAMETER_TIME] * 1000
|
|
|
|
partial_map = self._decode_map_partial(pmap, timestamp)
|
|
|
|
if partial_map:
|
|
if partial_map.frame_type == MapFrameType.I.value:
|
|
self._add_map_data(partial_map)
|
|
else:
|
|
self._queue_partial_map(partial_map)
|
|
|
|
if self._current_frame_id:
|
|
next_frame_id = self._current_frame_id + 1
|
|
|
|
partial_map = self._unqueue_partial_map(
|
|
self._latest_map_id, next_frame_id
|
|
)
|
|
if partial_map:
|
|
self._add_map_data(partial_map)
|
|
else:
|
|
if len(object_name_result) == 0:
|
|
self._delete_invalid_partial_maps()
|
|
tmpLen = self._partial_map_queue_size()
|
|
if tmpLen > 8:
|
|
self.request_new_map()
|
|
elif tmpLen > 4:
|
|
self._request_missing_p_map()
|
|
elif tmpLen > 0 and len(map_data_result) > 0:
|
|
self._request_next_p_map(self._latest_map_id, next_frame_id)
|
|
|
|
if len(object_name_result) == 1:
|
|
object_name = json.loads(
|
|
object_name_result[0][MAP_PARAMETER_VALUE])
|
|
if object_name:
|
|
_LOGGER.info("New object name received: %s", object_name[0])
|
|
timestamp = None
|
|
if object_name_result[0].get(MAP_PARAMETER_TIME):
|
|
timestamp = object_name_result[0][MAP_PARAMETER_TIME] * 1000
|
|
response, key = self._get_object_file_data(
|
|
object_name[0], timestamp)
|
|
if response:
|
|
partial_map = self._decode_map_partial(
|
|
response.decode(), timestamp, key
|
|
)
|
|
if partial_map:
|
|
if self._map_data is None or partial_map.frame_type == MapFrameType.I.value:
|
|
return self._add_map_data(partial_map)
|
|
|
|
self._queue_partial_map(partial_map)
|
|
next_partial_map = self._unqueue_next_partial_map()
|
|
if next_partial_map:
|
|
self._add_map_data(next_partial_map)
|
|
else:
|
|
self._delete_invalid_partial_maps()
|
|
if self._partial_map_queue_size() > 8:
|
|
self.request_new_map()
|
|
|
|
return len(map_data_result) or len(object_name_result)
|
|
|
|
def _request_map(self, parameters: dict[str, Any] = None) -> dict[str, Any] | None:
|
|
if parameters is None:
|
|
parameters = {
|
|
MAP_REQUEST_PARAMETER_FRAME_TYPE: MapFrameType.I.name,
|
|
}
|
|
|
|
payload = [{"piid": PIID(DreameVacuumProperty.FRAME_INFO), MAP_PARAMETER_VALUE: str(
|
|
json.dumps(parameters, separators=(",", ":"))).replace(" ", "")}]
|
|
|
|
try:
|
|
_LOGGER.info("Request map from device %s", payload)
|
|
mapping = DreameVacuumActionMapping[DreameVacuumAction.REQUEST_MAP]
|
|
return self._protocol.action(mapping["siid"], mapping["aiid"], payload, 0)
|
|
except Exception as ex:
|
|
_LOGGER.warning("Send request map failed: %s", ex)
|
|
return None
|
|
|
|
def _request_i_map(self, start_time: int = None) -> bool:
|
|
if not self._request_i_map_available:
|
|
return self.request_new_map()
|
|
|
|
parameters = {
|
|
MAP_REQUEST_PARAMETER_REQ_TYPE: 1,
|
|
MAP_REQUEST_PARAMETER_FRAME_TYPE: MapFrameType.I.name,
|
|
MAP_REQUEST_PARAMETER_FORCE_TYPE: 1,
|
|
}
|
|
|
|
if start_time:
|
|
parameters[MAP_PARAMETER_TIME] = start_time
|
|
|
|
result = self._request_map(parameters)
|
|
if result and result[MAP_PARAMETER_CODE] == 0:
|
|
out = result[MAP_PARAMETER_OUT]
|
|
_LOGGER.info("Response from device %s", out)
|
|
has_map = False
|
|
object_name = None
|
|
raw_map_data = None
|
|
for prop in out:
|
|
value = prop[MAP_PARAMETER_VALUE]
|
|
if value != "":
|
|
piid = prop["piid"]
|
|
if piid == PIID(DreameVacuumProperty.OBJECT_NAME):
|
|
has_map = True
|
|
object_name = value
|
|
elif piid == PIID(DreameVacuumProperty.MAP_DATA):
|
|
has_map = True
|
|
raw_map_data = value
|
|
elif piid == PIID(DreameVacuumProperty.ROBOT_TIME):
|
|
self._last_robot_time = int(value)
|
|
if start_time is None:
|
|
self._map_request_time = self._last_robot_time
|
|
self._map_request_count = 1
|
|
elif piid == PIID(DreameVacuumProperty.OLD_MAP_DATA):
|
|
if not has_map:
|
|
values = value.split(",")
|
|
if values[0] == "0":
|
|
raw_map_data = values[1]
|
|
else:
|
|
object_name = values[1]
|
|
if len(values) == 3:
|
|
object_name = f'{object_name},{values[2]}'
|
|
|
|
if has_map:
|
|
self._latest_object_name_time = int(
|
|
self._last_robot_time / 1000) + 1
|
|
self._map_request_time = None
|
|
|
|
if object_name:
|
|
self._add_map_data_file(object_name, self._last_robot_time)
|
|
if raw_map_data:
|
|
self._add_raw_map_data(raw_map_data, self._last_robot_time)
|
|
return True
|
|
|
|
self._request_map_from_cloud()
|
|
return False
|
|
|
|
def _request_missing_p_map(self) -> bool:
|
|
if self._map_data is None:
|
|
return
|
|
|
|
if self._partial_map_queue_size() == 0:
|
|
return
|
|
|
|
frame_id = self._current_frame_id + 1
|
|
map_id = self._current_map_id
|
|
|
|
if (
|
|
self._last_p_request_time is not None
|
|
and self._last_p_request_map_id == map_id
|
|
and self._last_p_request_frame_id == frame_id
|
|
and (time.time() - self._last_p_request_time) < 3
|
|
):
|
|
return
|
|
|
|
self._last_p_request_map_id = map_id
|
|
self._last_p_request_frame_id = frame_id
|
|
self._last_p_request_time = time.time()
|
|
|
|
result = self._request_map({
|
|
MAP_REQUEST_PARAMETER_MAP_ID: map_id,
|
|
MAP_REQUEST_PARAMETER_FRAME_ID: frame_id,
|
|
MAP_REQUEST_PARAMETER_FRAME_TYPE: MapFrameType.P.name,
|
|
})
|
|
return bool(result and result[MAP_PARAMETER_CODE] == 0)
|
|
|
|
def _request_next_p_map(self, map_id: int, frame_id: int) -> bool:
|
|
key = f"{map_id}:{frame_id}"
|
|
if key in self._request_queue and self._request_queue[key]:
|
|
return
|
|
|
|
self._request_queue[key] = True
|
|
|
|
result = self._request_map({
|
|
MAP_REQUEST_PARAMETER_MAP_ID: map_id,
|
|
MAP_REQUEST_PARAMETER_REQ_TYPE: 1,
|
|
MAP_REQUEST_PARAMETER_FRAME_ID: frame_id,
|
|
MAP_REQUEST_PARAMETER_FRAME_TYPE: MapFrameType.P.name,
|
|
})
|
|
if result and result[MAP_PARAMETER_CODE] == 0:
|
|
del self._request_queue[key]
|
|
|
|
object_name = None
|
|
raw_map_data = None
|
|
timestamp = None
|
|
|
|
for prop in result[MAP_PARAMETER_OUT]:
|
|
value = prop[MAP_PARAMETER_VALUE]
|
|
if value != "":
|
|
piid = prop["piid"]
|
|
if piid == PIID(DreameVacuumProperty.OBJECT_NAME):
|
|
object_name = value
|
|
elif piid == PIID(DreameVacuumProperty.MAP_DATA):
|
|
raw_map_data = value
|
|
elif piid == PIID(DreameVacuumProperty.ROBOT_TIME):
|
|
timestamp = int(value)
|
|
|
|
if object_name:
|
|
self._add_map_data_file(object_name, timestamp)
|
|
if raw_map_data:
|
|
_LOGGER.info("Lost P map received: %s:%s", map_id, frame_id)
|
|
self._add_raw_map_data(raw_map_data, timestamp)
|
|
|
|
if not raw_map_data and self._vslam_map and not object_name:
|
|
self.request_new_map()
|
|
return False
|
|
return True
|
|
return False
|
|
|
|
def _request_t_map(self) -> None:
|
|
result = self._request_map({MAP_REQUEST_PARAMETER_FRAME_TYPE: "T"})
|
|
if result and result[MAP_PARAMETER_CODE] == 0:
|
|
self.request_map_list()
|
|
|
|
def _request_current_map(self, map_request_time: int = None) -> bool:
|
|
if self._request_i_map_available:
|
|
return self._request_i_map(map_request_time)
|
|
|
|
return self._request_map_from_cloud()
|
|
|
|
def _map_data_changed(self) -> None:
|
|
if self._ready and self._update_callback:
|
|
_LOGGER.debug("Update callback")
|
|
self._update_callback()
|
|
|
|
def _update_task(self) -> None:
|
|
if self._update_timer is not None:
|
|
self._update_timer.cancel()
|
|
self._update_timer = None
|
|
|
|
start = time.time()
|
|
self.update()
|
|
self.schedule_update(max(self._update_interval - (time.time() - start), 1))
|
|
|
|
def _queue_partial_map(self, map_data) -> None:
|
|
if map_data.map_id != self._latest_map_id:
|
|
return
|
|
next_frame_id = 0
|
|
|
|
if self._current_map_id is not None and self._current_map_id == self._latest_map_id:
|
|
next_frame_id = self._current_frame_id + 1
|
|
|
|
if map_data.map_id not in self._map_data_queue:
|
|
self._map_data_queue[map_data.map_id] = {}
|
|
|
|
if map_data.frame_id < next_frame_id:
|
|
return
|
|
self._map_data_queue[map_data.map_id][map_data.frame_id] = map_data
|
|
|
|
def _delete_invalid_partial_maps(self) -> None:
|
|
if self._latest_map_id is None:
|
|
return
|
|
|
|
if self._current_frame_id is None:
|
|
return
|
|
|
|
frame_id = self._current_frame_id
|
|
map_data_queue = copy.deepcopy(self._map_data_queue)
|
|
for (k, v) in map_data_queue.items():
|
|
if k != self._latest_map_id:
|
|
del self._map_data_queue[k]
|
|
|
|
if (
|
|
self._latest_map_id not in self._map_data_queue
|
|
or not self._map_data_queue[self._latest_map_id]
|
|
):
|
|
return
|
|
|
|
map_data_queue = copy.deepcopy(
|
|
self._map_data_queue[self._latest_map_id])
|
|
for (k, v) in map_data_queue.items():
|
|
if k <= frame_id:
|
|
del self._map_data_queue[self._latest_map_id][k]
|
|
|
|
def _unqueue_next_partial_map(self) -> MapData | None:
|
|
if (
|
|
self._latest_map_id is None
|
|
or self._current_frame_id is None
|
|
or self._current_map_id != self._latest_map_id
|
|
):
|
|
return
|
|
|
|
frame_id = self._current_frame_id + 1
|
|
if (
|
|
self._latest_map_id not in self._map_data_queue
|
|
or not self._map_data_queue[self._latest_map_id]
|
|
or frame_id not in self._map_data_queue[self._latest_map_id]
|
|
):
|
|
return
|
|
|
|
map_data = self._map_data_queue[self._latest_map_id][frame_id]
|
|
|
|
if map_data:
|
|
del self._map_data_queue[self._latest_map_id][frame_id]
|
|
return map_data
|
|
|
|
def _unqueue_partial_map(self, map_id: int, frame_id: int) -> MapData | None:
|
|
if (
|
|
map_id in self._map_data_queue
|
|
and self._map_data_queue[map_id]
|
|
and frame_id in self._map_data_queue[map_id]
|
|
):
|
|
map_data = self._map_data_queue[map_id][frame_id]
|
|
del self._map_data_queue[map_id][frame_id]
|
|
return map_data
|
|
|
|
def _partial_map_queue_size(self) -> int:
|
|
if self._latest_map_timestamp_ms is None:
|
|
return 0
|
|
|
|
if (
|
|
self._latest_map_id not in self._map_data_queue
|
|
or not self._map_data_queue[self._latest_map_id]
|
|
):
|
|
return 0
|
|
|
|
return len(self._map_data_queue[self._latest_map_id])
|
|
|
|
def _get_object_file_data(self, object_name: str = "", timestamp=None) -> Tuple[Any, Optional[str]]:
|
|
key = None
|
|
if object_name and "," in object_name:
|
|
values = object_name.split(",")
|
|
object_name = values[0]
|
|
key = values[1]
|
|
response = self._get_interim_file_data(object_name, timestamp)
|
|
return response, key
|
|
|
|
def _get_interim_file_data(self, object_name: str = "", timestamp=None) -> str | None:
|
|
if self._protocol.cloud.logged_in:
|
|
if object_name is None or object_name == "":
|
|
_LOGGER.debug("Get object name from cloud")
|
|
object_name_result = self._protocol.cloud.get_device_property(
|
|
DIID(DreameVacuumProperty.OBJECT_NAME)
|
|
)
|
|
if object_name_result:
|
|
object_name_result = json.loads(
|
|
object_name_result[0][MAP_PARAMETER_VALUE])
|
|
object_name = object_name_result[0]
|
|
|
|
if object_name is None or object_name == "":
|
|
object_name = f"{str(self._protocol.cloud.user_id)}/{str(self._protocol.cloud.device_id)}/0"
|
|
|
|
url = self._get_interim_file_url(object_name)
|
|
if url:
|
|
_LOGGER.debug("Request map data from cloud %s", url)
|
|
response = self._protocol.cloud.get_file(url)
|
|
if response is not None:
|
|
return response
|
|
_LOGGER.warning("Request map data from cloud failed %s", url)
|
|
if self._file_urls.get(object_name):
|
|
del self._file_urls[object_name]
|
|
|
|
def _get_interim_file_url(self, object_name: str) -> str | None:
|
|
url = None
|
|
if self._file_urls and self._file_urls.get(object_name):
|
|
object = self._file_urls[object_name]
|
|
now = int(round(time.time()))
|
|
if object[MAP_PARAMETER_EXPIRES_TIME] - now > 60:
|
|
url = f'{object[MAP_PARAMETER_URL]}¤t={str(now)}'
|
|
|
|
if url is None:
|
|
response = self._protocol.cloud.get_interim_file_url(object_name)
|
|
if response and response.get(MAP_PARAMETER_RESULT):
|
|
self._file_urls[object_name] = response[MAP_PARAMETER_RESULT]
|
|
url = self._file_urls[object_name][MAP_PARAMETER_URL]
|
|
return url
|
|
|
|
def _decode_map_partial(self, raw_map, timestamp=None, key=None) -> MapDataPartial | None:
|
|
partial_map = DreameVacuumMapDecoder.decode_map_partial(raw_map, self._aes_iv, key)
|
|
if partial_map is not None:
|
|
# After restart or unsuccessful start robot returns timestamp_ms as uptime and that messes up with the latest map/frame id detection.
|
|
# I could not figure out how app handles with this issue but i have added this code to update time stamp as request/object time.
|
|
|
|
if timestamp and (
|
|
partial_map.timestamp_ms is None
|
|
or partial_map.timestamp_ms < 1577826000000
|
|
):
|
|
partial_map.timestamp_ms = timestamp
|
|
|
|
if (
|
|
self._latest_map_timestamp_ms is None
|
|
or partial_map.timestamp_ms > self._latest_map_timestamp_ms
|
|
):
|
|
self._latest_map_timestamp_ms = partial_map.timestamp_ms
|
|
self._latest_map_id = partial_map.map_id
|
|
|
|
return partial_map
|
|
|
|
def _add_map_data_file(self, object_name: str, timestamp) -> None:
|
|
response, key = self._get_object_file_data(object_name, timestamp)
|
|
if response is not None:
|
|
self._add_raw_map_data(response.decode(), timestamp, key)
|
|
|
|
def _add_raw_map_data(self, raw_map: str, timestamp=None, key=None) -> bool:
|
|
return self._add_map_data(self._decode_map_partial(raw_map, timestamp, key))
|
|
|
|
def _add_map_data(self, partial_map: MapDataPartial) -> None:
|
|
if partial_map is not None:
|
|
if (
|
|
partial_map.timestamp_ms is not None
|
|
and self._current_timestamp_ms is not None
|
|
and self._current_timestamp_ms is not None
|
|
and self._current_frame_id
|
|
and self._current_timestamp_ms > partial_map.timestamp_ms
|
|
):
|
|
_LOGGER.debug(
|
|
"Skip frame %s, timestamp %s:%s < %s:%s",
|
|
partial_map.frame_type,
|
|
partial_map.frame_id,
|
|
partial_map.timestamp_ms,
|
|
self._current_frame_id,
|
|
self._current_timestamp_ms,
|
|
)
|
|
return
|
|
|
|
if (
|
|
self._current_map_id is not None
|
|
and self._current_map_id != self._latest_map_id
|
|
):
|
|
_LOGGER.info(
|
|
"Map ID Changed: %s -> %s",
|
|
self._current_map_id,
|
|
self._latest_map_id,
|
|
)
|
|
|
|
self._current_frame_id = None
|
|
self._current_map_id = None
|
|
self._updated_frame_id = None
|
|
# self.request_next_map_list()
|
|
|
|
if partial_map.map_id != self._latest_map_id:
|
|
_LOGGER.info(
|
|
"Skip frame, map_id %s != %s",
|
|
partial_map.map_id,
|
|
self._latest_map_id,
|
|
)
|
|
# self._add_next_map_data()
|
|
return
|
|
|
|
if (
|
|
self._current_frame_id is not None
|
|
and self._current_frame_id is not None
|
|
and partial_map.frame_id < self._current_frame_id
|
|
):
|
|
if (
|
|
partial_map.frame_type != MapFrameType.I.value
|
|
or partial_map.timestamp_ms <= self._current_timestamp_ms
|
|
):
|
|
_LOGGER.info(
|
|
"Skip frame, frame id %s:%s < %s:%s",
|
|
partial_map.map_id,
|
|
partial_map.frame_id,
|
|
self._current_map_id,
|
|
self._current_frame_id,
|
|
)
|
|
# self._add_next_map_data()
|
|
return
|
|
|
|
if partial_map.frame_type == MapFrameType.P.value:
|
|
if (
|
|
self._current_frame_id is not None
|
|
and self._map_data is not None
|
|
and self._map_data.restored_map
|
|
):
|
|
_LOGGER.debug("Current map data removed")
|
|
self._map_data = None
|
|
self._current_frame_id = None
|
|
self._current_map_id = None
|
|
|
|
if self._current_frame_id is None or self._map_data is None:
|
|
self._queue_partial_map(partial_map)
|
|
|
|
if self._map_request_time is None:
|
|
self._request_i_map()
|
|
return
|
|
|
|
if partial_map.frame_id != self._current_frame_id + 1:
|
|
if partial_map.frame_id <= self._current_frame_id:
|
|
self._add_next_map_data()
|
|
return
|
|
|
|
self._queue_partial_map(partial_map)
|
|
self._delete_invalid_partial_maps()
|
|
|
|
if self._partial_map_queue_size() > 0:
|
|
self._request_next_p_map(
|
|
partial_map.map_id, self._current_frame_id + 1
|
|
)
|
|
else:
|
|
self._add_next_map_data()
|
|
return
|
|
|
|
current_robot_position = copy.deepcopy(
|
|
self._map_data.robot_position) if self._map_data.robot_position else None
|
|
|
|
map_data = DreameVacuumMapDecoder.decode_p_map_data_from_partial(
|
|
partial_map, self._map_data, self._vslam_map,
|
|
)
|
|
if map_data:
|
|
self._map_data = map_data
|
|
self._map_data.last_updated = time.time()
|
|
self._updated_frame_id = None
|
|
self._current_frame_id = map_data.frame_id
|
|
self._current_map_id = map_data.map_id
|
|
self._current_timestamp_ms = map_data.timestamp_ms
|
|
|
|
_LOGGER.info(
|
|
"Decode P map %d %d", map_data.map_id, map_data.frame_id
|
|
)
|
|
|
|
if not self._device_running or current_robot_position != map_data.robot_position:
|
|
self._map_data_changed()
|
|
|
|
elif partial_map.frame_type == MapFrameType.I.value:
|
|
self._need_map_request = False
|
|
self._delete_invalid_partial_maps()
|
|
|
|
(
|
|
map_data,
|
|
saved_map_data,
|
|
) = DreameVacuumMapDecoder.decode_map_data_from_partial(partial_map, self._vslam_map)
|
|
if map_data is None:
|
|
self._add_next_map_data()
|
|
return
|
|
|
|
if map_data.empty_map:
|
|
if self._map_data is None or not self._map_data.empty_map:
|
|
self._init_data()
|
|
self._map_data = map_data
|
|
self._current_frame_id = map_data.frame_id
|
|
self._current_map_id = map_data.map_id
|
|
self._current_timestamp_ms = map_data.timestamp_ms
|
|
|
|
self._map_data_changed()
|
|
self._add_next_map_data()
|
|
return
|
|
|
|
if saved_map_data is not None and saved_map_data.saved_map:
|
|
if saved_map_data.map_id in self._saved_map_data:
|
|
map_data.temporary_map = False
|
|
self._selected_map_id = saved_map_data.map_id
|
|
saved_map_data.map_name = self._saved_map_data[
|
|
saved_map_data.map_id
|
|
].map_name
|
|
saved_map_data.custom_name = self._saved_map_data[
|
|
saved_map_data.map_id
|
|
].custom_name
|
|
saved_map_data.rotation = self._saved_map_data[
|
|
saved_map_data.map_id
|
|
].rotation
|
|
saved_map_data.map_index = self._saved_map_data[
|
|
saved_map_data.map_id
|
|
].map_index
|
|
|
|
saved_map_data.timestamp_ms = map_data.timestamp_ms
|
|
if (
|
|
saved_map_data
|
|
!= self._saved_map_data[saved_map_data.map_id]
|
|
or saved_map_data.segments
|
|
!= self._saved_map_data[saved_map_data.map_id].segments
|
|
):
|
|
saved_map_data.last_updated = time.time()
|
|
self._saved_map_data[saved_map_data.map_id] = saved_map_data
|
|
|
|
_LOGGER.debug(
|
|
"Decode saved map %s: %s",
|
|
saved_map_data.map_id,
|
|
saved_map_data.map_name,
|
|
)
|
|
elif not map_data.temporary_map:
|
|
if not self._map_list:
|
|
saved_map_data.last_updated = time.time()
|
|
self._saved_map_data[saved_map_data.map_id] = saved_map_data
|
|
|
|
_LOGGER.info(
|
|
"Add saved map from new map %s", saved_map_data.map_id
|
|
)
|
|
self._refresh_map_list()
|
|
if self._map_data:
|
|
self._map_data_changed()
|
|
|
|
if self._device_running:
|
|
self.request_next_map_list()
|
|
else:
|
|
self.request_map_list()
|
|
|
|
if not map_data.saved_map:
|
|
if self._vslam_map:
|
|
if map_data.saved_map_status == 1 and saved_map_data and self._device_docked:
|
|
map_data.segments = copy.deepcopy(saved_map_data.segments)
|
|
map_data.data = copy.deepcopy(saved_map_data.data)
|
|
map_data.pixel_type = copy.deepcopy(saved_map_data.pixel_type)
|
|
map_data.dimensions = copy.deepcopy(saved_map_data.dimensions)
|
|
map_data.charger_position = copy.deepcopy(saved_map_data.charger_position)
|
|
map_data.no_go_areas = saved_map_data.no_go_areas
|
|
map_data.no_mopping_areas = saved_map_data.no_mopping_areas
|
|
map_data.walls = saved_map_data.walls
|
|
map_data.robot_position = None
|
|
map_data.docked = True
|
|
#map_data.restored_map = True
|
|
map_data.path = None
|
|
map_data.need_optimization = False
|
|
map_data.saved_map_status = 2
|
|
elif map_data.robot_position is None and map_data.restored_map and not self._device_docked and self._map_data and not map_data.docked:
|
|
map_data.robot_position = self._map_data.robot_position
|
|
|
|
changed = (
|
|
self._current_frame_id is None
|
|
or self._map_data is None
|
|
or map_data != self._map_data
|
|
or map_data.segments != self._map_data.segments
|
|
)
|
|
|
|
if (
|
|
changed
|
|
or self._current_frame_id != map_data.frame_id
|
|
or self._current_timestamp_ms != map_data.timestamp_ms
|
|
):
|
|
if (
|
|
self._current_frame_id is not None
|
|
and self._map_data is not None
|
|
and self._updated_frame_id is not None
|
|
):
|
|
if map_data.frame_id <= self._updated_frame_id:
|
|
if (
|
|
not self._map_data.empty_map
|
|
and (self._map_data.saved_map_status == 2 or (self._vslam_map and self._map_data.saved_map_status == 1))
|
|
):
|
|
map_data.active_segments = (
|
|
self._map_data.active_segments
|
|
)
|
|
map_data.active_areas = self._map_data.active_areas
|
|
map_data.active_points = self._map_data.active_points
|
|
map_data.path = self._map_data.path
|
|
map_data.segments = self._map_data.segments
|
|
map_data.cleanset = self._map_data.cleanset
|
|
changed = map_data != self._map_data
|
|
else:
|
|
changed = False
|
|
map_data.empty_map = True
|
|
else:
|
|
self._updated_frame_id = None
|
|
|
|
if self._map_data and not changed and map_data.need_optimization and not self._map_data.need_optimization:
|
|
map_data.need_optimization = False
|
|
map_data.optimized_pixel_type = copy.deepcopy(self._map_data.optimized_pixel_type)
|
|
map_data.optimized_dimensions = copy.deepcopy(self._map_data.optimized_dimensions)
|
|
map_data.optimized_charger_position = copy.deepcopy(self._map_data.optimized_charger_position)
|
|
|
|
self._map_data = map_data
|
|
self._current_frame_id = map_data.frame_id
|
|
self._current_map_id = map_data.map_id
|
|
self._current_timestamp_ms = map_data.timestamp_ms
|
|
|
|
if changed:
|
|
_LOGGER.info(
|
|
"Decode I map %d %d", map_data.map_id, map_data.frame_id
|
|
)
|
|
|
|
self._map_data.last_updated = time.time()
|
|
self._map_data_changed()
|
|
else:
|
|
_LOGGER.info(
|
|
"Decode map %d %d not changed",
|
|
map_data.map_id,
|
|
map_data.frame_id,
|
|
)
|
|
|
|
if self._current_frame_id is None and self._map_data is not None:
|
|
self._map_data = None
|
|
self._map_data_changed()
|
|
|
|
self._add_next_map_data()
|
|
|
|
def _add_next_map_data(self) -> None:
|
|
next_partial_map = self._unqueue_next_partial_map()
|
|
if next_partial_map is not None:
|
|
_LOGGER.debug("Continue to next map data")
|
|
self._add_map_data(next_partial_map)
|
|
|
|
def _refresh_map_list(self) -> None:
|
|
index = 1
|
|
new_map_list = []
|
|
for (map_id, saved_map_data) in sorted(self._saved_map_data.items()):
|
|
new_map_list.append(map_id)
|
|
if saved_map_data.custom_name is None:
|
|
saved_map_data.map_name = f"Map {str(index)}"
|
|
else:
|
|
saved_map_data.map_name = saved_map_data.custom_name
|
|
saved_map_data.map_index = index
|
|
index = index + 1
|
|
self._map_list = new_map_list
|
|
|
|
def get_map(self, map_index: int = 0) -> MapData | None:
|
|
if map_index:
|
|
if map_index <= len(self._map_list):
|
|
return self._saved_map_data[self._map_list[map_index - 1]]
|
|
return None
|
|
return self._map_data
|
|
|
|
def listen(self, callback) -> None:
|
|
self._update_callback = callback
|
|
|
|
def listen_error(self, callback) -> None:
|
|
self._error_callback = callback
|
|
|
|
def schedule_update(self, wait: float = None) -> None:
|
|
if not wait:
|
|
wait = self._update_interval
|
|
if self._update_timer is not None:
|
|
self._update_timer.cancel()
|
|
del self._update_timer
|
|
self._update_timer = None
|
|
if wait >= 0:
|
|
self._update_timer = Timer(wait, self._update_task)
|
|
self._update_timer.start()
|
|
|
|
def update(self) -> None:
|
|
if self._update_running:
|
|
return
|
|
|
|
self._update_running = True
|
|
|
|
_LOGGER.debug("Map update: %s", self._update_interval)
|
|
try:
|
|
if (self._map_list_object_name and self._need_map_list_request is None) or (self._need_map_list_request and not self._device_running):
|
|
self.request_map_list()
|
|
|
|
# Not supported Yet
|
|
# if self._recovery_map_list_object_name and self._need_recovery_map_list_request is None or (self._need_recovery_map_list_request and not self._device_running):
|
|
# self.request_recovery_map_list()
|
|
|
|
if self._map_request_time is not None or self._need_map_request:
|
|
self._updated_frame_id = None
|
|
self._map_request_count = self._map_request_count + 1
|
|
if self._map_request_count >= 16:
|
|
self._map_request_time = None
|
|
self._need_map_request = False
|
|
else:
|
|
self._request_current_map(self._map_request_time)
|
|
elif self._map_data is None or (
|
|
self._device_running
|
|
and (
|
|
time.time() - (self._current_timestamp_ms / 1000.0) > 15
|
|
or self._map_data.empty_map
|
|
)
|
|
):
|
|
self._updated_frame_id = None
|
|
if self._map_data and not self._map_data.empty_map:
|
|
_LOGGER.info(
|
|
"Need map request: %.2f",
|
|
time.time() - (self._current_timestamp_ms / 1000.0),
|
|
)
|
|
# if self._map_data and not self._map_data.empty_map and time.time() - (self._current_timestamp_ms / 1000.0) > 30:
|
|
# self.request_new_map()
|
|
# else:
|
|
if self._protocol.cloud.logged_in:
|
|
self._request_current_map()
|
|
elif not self._request_map_from_cloud() and self._device_running:
|
|
_LOGGER.info("No new map data received, retrying")
|
|
sleep(0.5)
|
|
if not self._request_map_from_cloud():
|
|
_LOGGER.info(
|
|
"No new map data received on second try")
|
|
|
|
if not self._available:
|
|
self._available = True
|
|
self._map_data_changed()
|
|
except Exception as ex:
|
|
if self._available:
|
|
_LOGGER.warning("Map update Failed: %s",
|
|
traceback.format_exc())
|
|
self._available = False
|
|
if self._error_callback:
|
|
self._error_callback(DeviceUpdateFailedException(ex))
|
|
|
|
self._ready = True
|
|
self._update_running = False
|
|
|
|
def set_aes_iv(self, aes_iv: str) -> None:
|
|
if aes_iv:
|
|
self._aes_iv = aes_iv
|
|
|
|
def set_vslam_map(self) -> None:
|
|
self._vslam_map = True
|
|
|
|
def set_update_interval(self, update_interval: float) -> None:
|
|
if self._update_interval != update_interval:
|
|
self._update_interval = update_interval
|
|
self.schedule_update()
|
|
|
|
def set_device_running(self, running: bool, docked: bool) -> None:
|
|
if self._device_running != running or self._device_docked != docked:
|
|
self._device_running = running
|
|
if self._device_docked != docked:
|
|
if self._vslam_map and docked and self._map_data and self._map_data.saved_map_status == 1:
|
|
saved_map_data = self._map_manager.selected_map
|
|
self._map_data.segments = copy.deepcopy(saved_map_data.segments)
|
|
self._map_data.data = copy.deepcopy(saved_map_data.data)
|
|
self._map_data.pixel_type = copy.deepcopy(saved_map_data.pixel_type)
|
|
self._map_data.dimensions = copy.deepcopy(saved_map_data.dimensions)
|
|
self._map_data.charger_position = copy.deepcopy(saved_map_data.charger_position)
|
|
self._map_data.no_go_areas = saved_map_data.no_go_areas
|
|
self._map_data.no_mopping_areas = saved_map_data.no_mopping_areas
|
|
self._map_data.walls = saved_map_data.walls
|
|
self._map_data.robot_position = self._map_data.charger_position
|
|
self._map_data.docked = True
|
|
#self._map_data.restored_map = True
|
|
self._map_data.path = None
|
|
self._map_data.need_optimization = False
|
|
self._map_data.saved_map_status = 2
|
|
self._map_data.last_updated = time.time()
|
|
self._map_data_changed()
|
|
|
|
self._device_docked = docked
|
|
|
|
self.schedule_update(2)
|
|
|
|
def set_device_docked(self, device_docked: bool) -> None:
|
|
if self._device_docked != device_docked:
|
|
self.schedule_update(2)
|
|
self._device_docked = device_docked
|
|
|
|
def request_new_map(self) -> None:
|
|
if self._new_map_request_time and time.time() - self._new_map_request_time < 10:
|
|
if time.time() - self._new_map_request_time > 3:
|
|
self._new_map_request_time = time.time()
|
|
self._request_map_from_cloud()
|
|
return
|
|
|
|
self._new_map_request_time = time.time()
|
|
if self._map_data is None:
|
|
return self._request_i_map()
|
|
else:
|
|
result = self._request_map()
|
|
if result and result[MAP_PARAMETER_CODE] == 0:
|
|
self._request_map_from_cloud()
|
|
|
|
def request_next_map(self) -> None:
|
|
self._map_request_count = 0
|
|
self._need_map_request = True
|
|
self.schedule_update(2)
|
|
|
|
def request_next_map_list(self) -> None:
|
|
self._need_map_list_request = True
|
|
|
|
def set_map_list_object_name(self, map_list: dict[int, str]) -> bool:
|
|
if map_list and map_list != "":
|
|
md5 = map_list.get(MAP_PARAMETER_MD5)
|
|
object_name = map_list.get(MAP_PARAMETER_OBJECT_NAME)
|
|
if map_list and object_name and object_name != "":
|
|
if self._map_list_object_name != object_name or self._map_list_md5 != md5:
|
|
self._map_list_object_name = object_name
|
|
if not self._device_running and self._map_list_md5 is not None:
|
|
self.request_next_map_list()
|
|
self.schedule_update(2)
|
|
self._map_list_md5 = md5
|
|
return True
|
|
return False
|
|
|
|
def set_recovery_map_list_object_name(self, map_list: dict[int, str]) -> bool:
|
|
if map_list and map_list != "":
|
|
object_name = map_list.get(MAP_PARAMETER_OBJECT_NAME)
|
|
if map_list and object_name and object_name != "":
|
|
if self._recovery_map_list_object_name != object_name:
|
|
self._recovery_map_list_object_name = object_name
|
|
self._need_recovery_map_list_request = True
|
|
return True
|
|
return False
|
|
|
|
def request_map_list(self) -> None:
|
|
if self._map_list_object_name and self._protocol.cloud.logged_in:
|
|
_LOGGER.info("Get Map List: %s", self._map_list_object_name)
|
|
try:
|
|
response = self._get_interim_file_data(
|
|
self._map_list_object_name)
|
|
except Exception as ex:
|
|
_LOGGER.warn("Get Map List failed: %s", ex)
|
|
return
|
|
|
|
if response:
|
|
self._need_map_list_request = False
|
|
raw_map = response.decode()
|
|
|
|
try:
|
|
map_info = json.loads(raw_map)
|
|
except:
|
|
_LOGGER.warn("Get Map List json parse failed")
|
|
return
|
|
|
|
saved_map_list = map_info[MAP_PARAMETER_MAPSTR]
|
|
changed = False
|
|
now = time.time()
|
|
map_list = {}
|
|
if saved_map_list:
|
|
for v in saved_map_list:
|
|
if v.get(MAP_PARAMETER_MAP):
|
|
saved_map_data = DreameVacuumMapDecoder.decode_saved_map(
|
|
v[MAP_PARAMETER_MAP], self._vslam_map, int(v[MAP_PARAMETER_ANGLE]) if v.get(
|
|
MAP_PARAMETER_ANGLE) else 0, self._aes_iv
|
|
)
|
|
if saved_map_data is not None:
|
|
name = v.get(MAP_PARAMETER_NAME)
|
|
if name:
|
|
saved_map_data.custom_name = name
|
|
saved_map_data.map_name = name
|
|
map_list[saved_map_data.map_id] = saved_map_data
|
|
|
|
for (map_id, saved_map_data) in sorted(map_list.items()):
|
|
if map_id in self._saved_map_data:
|
|
if self._selected_map_id == map_id and self._map_data:
|
|
saved_map_data.cleanset = self._map_data.cleanset
|
|
else:
|
|
saved_map_data.cleanset = self._saved_map_data[map_id].cleanset
|
|
|
|
if self._saved_map_data[map_id] != saved_map_data:
|
|
_LOGGER.info("Saved map changed: %s", map_id)
|
|
changed = True
|
|
saved_map_data.last_updated = now
|
|
if (
|
|
self._map_data is None
|
|
or self._selected_map_id != map_id
|
|
):
|
|
self._saved_map_data[map_id] = saved_map_data
|
|
else:
|
|
self._saved_map_data[
|
|
map_id
|
|
].custom_name = saved_map_data.custom_name
|
|
self._saved_map_data[
|
|
map_id
|
|
].rotation = saved_map_data.rotation
|
|
else:
|
|
saved_map_data.last_updated = now
|
|
self._saved_map_data[map_id] = saved_map_data
|
|
_LOGGER.info("Add saved map: %s", map_id)
|
|
changed = True
|
|
|
|
current_map_list = self._saved_map_data.copy()
|
|
for map_id in current_map_list.keys():
|
|
if map_id not in map_list:
|
|
del self._saved_map_data[map_id]
|
|
changed = True
|
|
|
|
selected_map_id = map_info[MAP_PARAMETER_CURR_ID]
|
|
if (
|
|
selected_map_id in self._saved_map_data
|
|
and self._selected_map_id != selected_map_id
|
|
):
|
|
self._selected_map_id = selected_map_id
|
|
changed = True
|
|
|
|
if changed == True:
|
|
self._refresh_map_list()
|
|
if self._map_data:
|
|
self._map_data_changed()
|
|
|
|
def request_recovery_map_list(self) -> None:
|
|
if self._recovery_map_list_object_name:
|
|
now = time.time()
|
|
response = self._get_interim_file_data(
|
|
self._recovery_map_list_object_name)
|
|
if response:
|
|
self._need_recovery_map_list_request = False
|
|
raw_map = response.decode()
|
|
recovery_map_list = json.loads(raw_map)
|
|
for recovery_map in recovery_map_list:
|
|
map_id = recovery_map[MAP_PARAMETER_ID]
|
|
if map_id in self._map_list:
|
|
map_info_list = recovery_map[MAP_PARAMETER_INFO]
|
|
for map_info in map_info_list:
|
|
first = map_info[MAP_PARAMETER_FIRST]
|
|
map_time = map_info[MAP_PARAMETER_TIME]
|
|
object_name = map_info[MAP_PARAMETER_OBJNAME]
|
|
if object_name.endswith('mb.tbz2'):
|
|
response = self._protocol.cloud.get_file_url(
|
|
object_name)
|
|
else:
|
|
response = self._protocol.cloud.get_interim_file_url(
|
|
object_name)
|
|
|
|
if response and response.get(MAP_PARAMETER_RESULT):
|
|
_LOGGER.info(
|
|
"Get recovery map file url result: %s", response)
|
|
map_url = response[MAP_PARAMETER_RESULT][MAP_PARAMETER_URL]
|
|
recovery_map_data = DreameVacuumMapDecoder.decode_saved_map(
|
|
map_info[MAP_PARAMETER_THB], self._vslam_map, self._saved_map_data[map_id].rotation, self._aes_iv)
|
|
# TODO: store recovery map
|
|
|
|
@property
|
|
def _request_i_map_available(self) -> bool:
|
|
return bool(
|
|
not (
|
|
self._map_data is not None
|
|
and (
|
|
(
|
|
self._map_data.saved_map_status == 0
|
|
and not self._map_data.empty_map
|
|
)
|
|
or self._map_data.saved_map_status == 1
|
|
or self._map_data.restored_map
|
|
or self._map_data.temporary_map
|
|
)
|
|
)
|
|
)
|
|
|
|
@property
|
|
def map_list(self) -> list[int] | None:
|
|
return self._saved_map_data.keys()
|
|
|
|
@property
|
|
def map_data_list(self) -> dict[int, MapData] | None:
|
|
return self._saved_map_data
|
|
|
|
@property
|
|
def selected_map(self) -> MapData | None:
|
|
if self._map_data:
|
|
if (
|
|
self._selected_map_id is not None
|
|
and self._selected_map_id in self._saved_map_data
|
|
):
|
|
return self._saved_map_data[self._selected_map_id]
|
|
|
|
if (
|
|
self._map_list
|
|
and len(self._map_list) == 1
|
|
and self._map_list[0] in self._saved_map_data
|
|
):
|
|
return self._saved_map_data[self._map_list[0]]
|
|
|
|
|
|
class DreameMapVacuumMapEditor:
|
|
""" Every map change must be handled on memory before actually requesting it to the device because it takes too much time to get the updated map from the cloud.
|
|
This class handles user edits on stored map data like updating customized cleaning settings or setting active segments on segment cleaning.
|
|
Original app has a similar class to handle the same issue (Works optimistically) """
|
|
|
|
def __init__(self, map_manager) -> None:
|
|
self._previous_cleaning_sequence: dict[int, list[int]] = {}
|
|
self.map_manager = map_manager
|
|
|
|
def _set_updated_frame_id(self, frame_id) -> None:
|
|
self.map_manager._updated_frame_id = frame_id
|
|
|
|
def refresh_map(self, map_id: int = None) -> None:
|
|
if map_id:
|
|
if self._saved_map_data and map_id in self._saved_map_data:
|
|
self._saved_map_data[map_id].last_updated = time.time()
|
|
return
|
|
if self._map_data is not None:
|
|
self._map_data.last_updated = time.time()
|
|
|
|
def set_active_areas(self, active_areas: list[list[int]]) -> None:
|
|
map_data = self._map_data
|
|
if map_data is not None:
|
|
map_data.active_areas = []
|
|
for area in active_areas:
|
|
x_coords = sorted([area[0], area[2]])
|
|
y_coords = sorted([area[1], area[3]])
|
|
map_data.active_areas.append(
|
|
Area(
|
|
x_coords[0],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[1],
|
|
x_coords[0],
|
|
y_coords[1],
|
|
)
|
|
)
|
|
self._set_updated_frame_id(map_data.frame_id)
|
|
self.refresh_map()
|
|
|
|
def set_active_segments(self, active_segments: list[int]) -> None:
|
|
map_data = self._map_data
|
|
if map_data is not None:
|
|
map_data.active_segments = active_segments
|
|
self._set_updated_frame_id(map_data.frame_id)
|
|
self.refresh_map()
|
|
|
|
def set_active_points(self, active_points: list[list[int]]) -> None:
|
|
map_data = self._map_data
|
|
if map_data is not None:
|
|
map_data.active_points = []
|
|
for point in active_points:
|
|
map_data.active_points.append(
|
|
Point(
|
|
point[0],
|
|
point[1],
|
|
)
|
|
)
|
|
self._set_updated_frame_id(map_data.frame_id)
|
|
self.refresh_map()
|
|
|
|
def clear_path(self) -> None:
|
|
map_data = self._map_data
|
|
if map_data is not None:
|
|
map_data.path = None
|
|
self._set_updated_frame_id(map_data.frame_id)
|
|
self.refresh_map()
|
|
|
|
def reset_map(self) -> None:
|
|
map_data = self._map_data
|
|
if map_data is not None:
|
|
map_data.dimensions.width = 0
|
|
map_data.dimensions.height = 0
|
|
map_data.segments = {}
|
|
map_data.path = None
|
|
map_data.empty_map = True
|
|
map_data.saved_map_status = 0
|
|
self._set_updated_frame_id(map_data.frame_id + 2)
|
|
self.refresh_map()
|
|
|
|
def set_rotation(self, map_id: int, rotation: int) -> None:
|
|
if map_id in self._saved_map_data:
|
|
self._saved_map_data[map_id].rotation = rotation
|
|
if self._map_data is not None and map_id == self._selected_map_id:
|
|
self._map_data.rotation = rotation
|
|
self.refresh_map()
|
|
self.refresh_map(map_id)
|
|
|
|
def set_map_name(self, map_id: int, name: str) -> None:
|
|
if map_id in self._saved_map_data:
|
|
self._saved_map_data[map_id].custom_name = name
|
|
self._saved_map_data[map_id].map_name = name
|
|
self.refresh_map(map_id)
|
|
self.refresh_map()
|
|
|
|
def select_map(self, map_id: int) -> None:
|
|
if map_id != self._selected_map_id:
|
|
self.set_current_map(map_id)
|
|
|
|
def set_current_map(self, map_id: int) -> None:
|
|
if map_id and map_id in self._saved_map_data:
|
|
saved_map_data = copy.deepcopy(self._saved_map_data[map_id])
|
|
saved_map_data.docked = self._map_data.docked
|
|
saved_map_data.timestamp_ms = self._current_timestamp_ms
|
|
saved_map_data.frame_id = None
|
|
saved_map_data.map_name = None
|
|
saved_map_data.map_index = 0
|
|
saved_map_data.custom_name = None
|
|
saved_map_data.saved_map = False
|
|
saved_map_data.restored_map = True
|
|
saved_map_data.temporary_map = False
|
|
saved_map_data.empty_map = False
|
|
saved_map_data.saved_map_status = 2
|
|
DreameVacuumMapDecoder.set_segment_cleanset(
|
|
saved_map_data, saved_map_data.cleanset
|
|
)
|
|
self.map_manager._map_data = saved_map_data
|
|
self.map_manager._current_frame_id = None
|
|
self.map_manager._current_map_id = map_id
|
|
self.map_manager._selected_map_id = map_id
|
|
self.refresh_map()
|
|
|
|
def delete_map(self, map_id: int = None) -> None:
|
|
map_data = self._map_data
|
|
if map_data and map_data.temporary_map:
|
|
return
|
|
|
|
if map_id is None:
|
|
self.map_manager._map_data = None
|
|
self.map_manager._selected_map_id = None
|
|
self.map_manager._updated_frame_id = None
|
|
self.map_manager._saved_map_data = {}
|
|
self.map_manager._refresh_map_list()
|
|
self.map_manager.request_next_map_list()
|
|
else:
|
|
if self._saved_map_data and map_id not in self._saved_map_data:
|
|
self.map_manager.schedule_update(2)
|
|
return
|
|
|
|
if map_data and self._selected_map_id == map_id:
|
|
if len(self.map_manager._map_list) > 1:
|
|
self.set_current_map(self.map_manager._map_list[-1])
|
|
else:
|
|
self.map_manager._map_data = None
|
|
self._updated_frame_id = None
|
|
self._selected_map_id = None
|
|
|
|
del self.map_manager._saved_map_data[map_id]
|
|
self.map_manager._refresh_map_list()
|
|
self.map_manager.request_next_map_list()
|
|
|
|
def merge_segments(self, map_id: int, segments: list[int]) -> None:
|
|
saved_map_data = self._saved_map_data
|
|
if (
|
|
saved_map_data
|
|
and map_id in saved_map_data
|
|
and len(segments) == 2
|
|
):
|
|
map_data = saved_map_data[map_id]
|
|
if (
|
|
map_data.segments
|
|
and segments[0] in map_data.segments
|
|
and segments[1] in map_data.segments
|
|
):
|
|
if segments[1] not in map_data.segments[segments[0]].neighbors:
|
|
_LOGGER.error(
|
|
"Segments are not neighbors with each other: %s", segments
|
|
)
|
|
return
|
|
|
|
data = np.zeros((map_data.dimensions.width *
|
|
map_data.dimensions.height), np.uint8)
|
|
for y in range(map_data.dimensions.height):
|
|
for x in range(map_data.dimensions.width):
|
|
index = y * map_data.dimensions.width + x
|
|
if (map_data.data[index] & 0x3F) == segments[1]:
|
|
data[index] = segments[0]
|
|
else:
|
|
data[index] = map_data.data[index]
|
|
|
|
if int(map_data.pixel_type[x, y]) == segments[1]:
|
|
map_data.pixel_type[x, y] = segments[0]
|
|
|
|
map_data.data = bytes(data)
|
|
del self.map_manager._saved_map_data[map_id].segments[segments[1]]
|
|
new_segments = DreameVacuumMapDecoder.get_segments(map_data, self.map_manager._vslam_map)
|
|
map_data.segments[segments[0]].x = new_segments[segments[0]].x
|
|
map_data.segments[segments[0]].y = new_segments[segments[0]].y
|
|
|
|
for (k, v) in map_data.segments.items():
|
|
if segments[1] in v.neighbors:
|
|
map_data.segments[k].neighbors.remove(segments[1])
|
|
|
|
DreameVacuumMapDecoder.set_segment_color_index(map_data)
|
|
if self._map_data and map_id == self._selected_map_id:
|
|
self.set_current_map(map_id)
|
|
self.refresh_map(map_id)
|
|
|
|
def split_segments(self, map_id: int, segment: int, line: list[int]) -> None:
|
|
if self._saved_map_data and map_id in self._saved_map_data:
|
|
if self._map_data and map_id == self._selected_map_id:
|
|
self.set_current_map(map_id)
|
|
self.refresh_map(map_id)
|
|
|
|
def save_temporary_map(self) -> None:
|
|
if self._map_data and self._map_data.temporary_map:
|
|
self._map_data.temporary_map = False
|
|
self.refresh_map()
|
|
self.map_manager.request_next_map_list()
|
|
|
|
def discard_temporary_map(self) -> None:
|
|
if self._map_data and self._map_data.temporary_map and self._selected_map_id:
|
|
self.set_current_map(self._selected_map_id)
|
|
self.map_manager.request_next_map_list()
|
|
|
|
def replace_temporary_map(self, map_id: int = None) -> None:
|
|
map_data = self._map_data
|
|
if map_data and map_data.temporary_map:
|
|
if not map_id and self._selected_map_id:
|
|
map_id = self._selected_map_id
|
|
|
|
if map_id in self._saved_map_data:
|
|
new_map = copy.deepcopy(map_data)
|
|
new_map.map_id = new_map.saved_map_id
|
|
new_map.saved_map_id = None
|
|
new_map.saved_map_status = -1
|
|
new_map.saved_map = True
|
|
new_map.cleanset = {}
|
|
self.map_manager._saved_map_data[new_map.map_id] = new_map
|
|
del self.map_manager._saved_map_data[map_id]
|
|
self.map_manager._refresh_map_list()
|
|
|
|
map_data.saved_map_id = new_map.map_id
|
|
map_data.temporary_map = False
|
|
map_data.saved_map = False
|
|
map_data.saved_map_status = 0
|
|
map_data.restored_map = True
|
|
map_data.empty_map = False
|
|
map_data.cleanset = {}
|
|
DreameVacuumMapDecoder.set_segment_cleanset(
|
|
map_data, map_data.cleanset
|
|
)
|
|
self.map_manager._map_data = map_data
|
|
self.map_manager._selected_map_id = new_map.map_id
|
|
self.map_manager.request_next_map_list()
|
|
self.refresh_map()
|
|
|
|
def restore_map(self, map_id: int, map_url: str) -> None:
|
|
self.map_manager.request_next_map_list()
|
|
|
|
def set_cleaning_sequence(self, cleaning_sequence: list[int]) -> list[int] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and not map_data.temporary_map
|
|
):
|
|
new_cleaning_sequence = []
|
|
if cleaning_sequence:
|
|
index = 1
|
|
for k in (
|
|
cleaning_sequence
|
|
if (
|
|
len(cleaning_sequence) == len(
|
|
map_data.segments.items())
|
|
and all(k in cleaning_sequence for k in map_data.segments.keys())
|
|
)
|
|
else sorted(map_data.segments.keys())
|
|
):
|
|
map_data.segments[k].order = index
|
|
map_data.cleanset[str(k)][3] = map_data.segments[k].order
|
|
new_cleaning_sequence.append(k)
|
|
index = index + 1
|
|
else:
|
|
for v in map_data.segments.values():
|
|
if v.order:
|
|
self._previous_cleaning_sequence[map_data.map_id] = [(k) for k, v in sorted(
|
|
map_data.segments.items(), key=lambda s: s[1].order) if v.order]
|
|
break
|
|
|
|
for k in map_data.segments.keys():
|
|
map_data.segments[k].order = 0
|
|
map_data.cleanset[str(k)][3] = 0
|
|
|
|
if self._saved_map_data and map_data.map_id in self._saved_map_data:
|
|
self._saved_map_data[map_data.map_id].cleanset = copy.deepcopy(
|
|
map_data.cleanset)
|
|
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return [(k) for k, v in sorted(map_data.segments.items(), key=lambda s: s[1].order) if v.order]
|
|
|
|
def set_segment_order(self, segment_id: int, order: int) -> list[int] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and segment_id in map_data.segments
|
|
and not map_data.temporary_map
|
|
):
|
|
if order > 0:
|
|
if (
|
|
not map_data.segments[segment_id].order
|
|
and map_data.map_id in self._previous_cleaning_sequence
|
|
and len(self._previous_cleaning_sequence[map_data.map_id]) == len(map_data.segments.items())
|
|
and all(k in self._previous_cleaning_sequence[map_data.map_id] for k in map_data.segments.keys())
|
|
):
|
|
cleaning_sequence = self.set_cleaning_sequence(
|
|
self._previous_cleaning_sequence[map_data.map_id])
|
|
del self._previous_cleaning_sequence[map_data.map_id]
|
|
return cleaning_sequence
|
|
|
|
index = 1
|
|
for k in sorted(map_data.segments.keys()):
|
|
if not map_data.segments[k].order:
|
|
map_data.segments[k].order = index
|
|
map_data.cleanset[str(
|
|
k)][3] = map_data.segments[k].order
|
|
index = index + 1
|
|
|
|
current_order = map_data.segments[segment_id].order
|
|
if current_order != order:
|
|
map_data.segments[segment_id].order = order
|
|
map_data.cleanset[str(segment_id)][3] = order
|
|
for k, v in map_data.segments.items():
|
|
if k != segment_id and v.order == order:
|
|
map_data.segments[k].order = current_order
|
|
map_data.cleanset[str(
|
|
k)][3] = map_data.segments[k].order
|
|
else:
|
|
for v in map_data.segments.values():
|
|
if v.order:
|
|
self._previous_cleaning_sequence[map_data.map_id] = [(k) for k, v in sorted(
|
|
map_data.segments.items(), key=lambda s: s[1].order) if v.order]
|
|
break
|
|
|
|
for k in map_data.segments.keys():
|
|
map_data.segments[k].order = 0
|
|
map_data.cleanset[str(k)][3] = 0
|
|
|
|
if self._saved_map_data and map_data.map_id in self._saved_map_data:
|
|
self._saved_map_data[map_data.map_id].cleanset = copy.deepcopy(
|
|
map_data.cleanset)
|
|
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return [(k) for k, v in sorted(map_data.segments.items(), key=lambda s: s[1].order) if v.order]
|
|
|
|
def cleanset(self, map_data: MapData) -> list[list[int]] | None:
|
|
cleanset = []
|
|
for (k, v) in map_data.segments.items():
|
|
if v.suction_level is None:
|
|
v.suction_level = 1
|
|
if v.water_volume is None:
|
|
v.water_volume = 2
|
|
if v.cleaning_times is None:
|
|
v.cleaning_times = 1
|
|
|
|
settings = [
|
|
k,
|
|
v.suction_level,
|
|
v.water_volume + 1,
|
|
v.cleaning_times,
|
|
]
|
|
|
|
if v.cleaning_mode is not None:
|
|
settings.append(v.cleaning_mode)
|
|
|
|
cleanset.append(settings)
|
|
return cleanset
|
|
|
|
def set_segment_suction_level(self, segment_id: int, suction_level: int) -> list[list[int]] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and segment_id in map_data.segments
|
|
and not map_data.temporary_map
|
|
):
|
|
map_data.segments[segment_id].suction_level = suction_level
|
|
map_data.cleanset[str(segment_id)][0] = suction_level
|
|
if self._saved_map_data and map_data.map_id in self._saved_map_data:
|
|
self._saved_map_data[map_data.map_id].cleanset = copy.deepcopy(
|
|
map_data.cleanset)
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return self.cleanset(map_data)
|
|
|
|
def set_segment_water_volume(self, segment_id: int, water_volume: int) -> list[list[int]] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and segment_id in map_data.segments
|
|
):
|
|
map_data.segments[segment_id].water_volume = water_volume
|
|
map_data.cleanset[str(segment_id)][1] = water_volume + 1
|
|
if self._saved_map_data and map_data.map_id in self._saved_map_data:
|
|
self._saved_map_data[map_data.map_id].cleanset = copy.deepcopy(
|
|
map_data.cleanset)
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return self.cleanset(map_data)
|
|
|
|
def set_segment_cleaning_times(self, segment_id: int, cleaning_times: int) -> list[list[int]] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and segment_id in map_data.segments
|
|
and not map_data.temporary_map
|
|
):
|
|
map_data.segments[segment_id].cleaning_times = cleaning_times
|
|
map_data.cleanset[str(segment_id)][2] = cleaning_times
|
|
if self._saved_map_data and map_data.map_id in self._saved_map_data:
|
|
self._saved_map_data[map_data.map_id].cleanset = copy.deepcopy(
|
|
map_data.cleanset)
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return self.cleanset(map_data)
|
|
|
|
def set_segment_cleaning_mode(self, segment_id: int, cleaning_mode: int) -> list[list[int]] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and segment_id in map_data.segments
|
|
and not map_data.temporary_map
|
|
):
|
|
map_data.segments[segment_id].cleaning_mode = cleaning_mode
|
|
map_data.cleanset[str(segment_id)][4] = cleaning_mode
|
|
if self._saved_map_data and map_data.map_id in self._saved_map_data:
|
|
self._saved_map_data[map_data.map_id].cleanset = copy.deepcopy(
|
|
map_data.cleanset)
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return self.cleanset(map_data)
|
|
|
|
def set_segment_name(self, segment_id: int, segment_type: int, custom_name: str = None) -> dict[str, Any] | None:
|
|
map_data = self._map_data
|
|
if (
|
|
map_data
|
|
and map_data.segments
|
|
and segment_id in map_data.segments
|
|
and self._selected_map_id
|
|
and not map_data.temporary_map
|
|
):
|
|
if (
|
|
map_data.segments[segment_id].type != segment_type
|
|
or map_data.segments[segment_id].custom_name != custom_name
|
|
):
|
|
segment_info = {}
|
|
map_data.segments[segment_id].type = segment_type
|
|
if segment_type == 0:
|
|
map_data.segments[segment_id].index = 0
|
|
if custom_name is not None:
|
|
if custom_name == "":
|
|
custom_name = None
|
|
map_data.segments[segment_id].custom_name = custom_name
|
|
else:
|
|
map_data.segments[segment_id].custom_name = None
|
|
map_data.segments[segment_id].index = map_data.segments[
|
|
segment_id
|
|
].next_type_index(segment_type, map_data.segments)
|
|
|
|
map_data.segments[segment_id].set_name()
|
|
|
|
self._saved_map_data[self._selected_map_id].segments[
|
|
segment_id
|
|
].custom_name = map_data.segments[segment_id].custom_name
|
|
self._saved_map_data[self._selected_map_id].segments[
|
|
segment_id
|
|
].index = map_data.segments[segment_id].index
|
|
self._saved_map_data[self._selected_map_id].segments[
|
|
segment_id
|
|
].type = map_data.segments[segment_id].type
|
|
self._saved_map_data[self._selected_map_id].segments[
|
|
segment_id
|
|
].set_name()
|
|
self.refresh_map(self._selected_map_id)
|
|
|
|
for (k, v) in map_data.segments.items():
|
|
if map_data.segments[k].custom_name is not None:
|
|
segment_info[k] = {MAP_PARAMETER_NAME: base64.b64encode(
|
|
map_data.segments[k].custom_name.encode("utf-8")
|
|
).decode("utf-8"), MAP_REQUEST_PARAMETER_TYPE: 0, MAP_REQUEST_PARAMETER_INDEX: 0}
|
|
elif map_data.segments[k].type:
|
|
segment_info[k] = {MAP_REQUEST_PARAMETER_TYPE: map_data.segments[k].type,
|
|
MAP_REQUEST_PARAMETER_INDEX: map_data.segments[k].index}
|
|
else:
|
|
segment_info[k] = {}
|
|
|
|
if map_data.segments[k].unique_id:
|
|
segment_info[k][MAP_REQUEST_PARAMETER_ROOM_ID] = map_data.segments[k].unique_id
|
|
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map()
|
|
return segment_info
|
|
|
|
def set_zones(self, walls, no_go_areas, no_mopping_areas) -> None:
|
|
map_data = self._map_data
|
|
if not map_data or not self._selected_map_id:
|
|
return
|
|
|
|
map_data.no_mopping_areas = []
|
|
if no_mopping_areas:
|
|
for area in no_mopping_areas:
|
|
x_coords = sorted([area[0], area[2]])
|
|
y_coords = sorted([area[1], area[3]])
|
|
map_data.no_mopping_areas.append(
|
|
Area(
|
|
x_coords[0],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[1],
|
|
x_coords[0],
|
|
y_coords[1],
|
|
)
|
|
)
|
|
|
|
map_data.no_go_areas = []
|
|
if no_go_areas:
|
|
for area in no_go_areas:
|
|
x_coords = sorted([area[0], area[2]])
|
|
y_coords = sorted([area[1], area[3]])
|
|
map_data.no_go_areas.append(
|
|
Area(
|
|
x_coords[0],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[1],
|
|
x_coords[0],
|
|
y_coords[1],
|
|
)
|
|
)
|
|
|
|
if walls:
|
|
map_data.walls = [
|
|
Wall(
|
|
wall[0],
|
|
wall[1],
|
|
wall[2],
|
|
wall[3],
|
|
)
|
|
for wall in walls
|
|
]
|
|
else:
|
|
map_data.walls = []
|
|
|
|
self._saved_map_data[
|
|
self._selected_map_id
|
|
].no_go_areas = map_data.no_go_areas
|
|
self._saved_map_data[
|
|
self._selected_map_id
|
|
].no_mopping_areas = map_data.no_mopping_areas
|
|
self._saved_map_data[self._selected_map_id].walls = map_data.walls
|
|
self._set_updated_frame_id(map_data.frame_id + 1)
|
|
self.refresh_map(self._selected_map_id)
|
|
self.refresh_map()
|
|
|
|
@property
|
|
def _map_data(self) -> MapData | None:
|
|
return self.map_manager._map_data
|
|
|
|
@property
|
|
def _saved_map_data(self) -> MapData | None:
|
|
return self.map_manager._saved_map_data
|
|
|
|
@property
|
|
def _selected_map_id(self) -> int | None:
|
|
return self.map_manager._selected_map_id
|
|
|
|
@property
|
|
def _current_timestamp_ms(self) -> int | None:
|
|
return self.map_manager._current_timestamp_ms
|
|
|
|
|
|
class DreameVacuumMapDecoder:
|
|
HEADER_SIZE = 27
|
|
|
|
@staticmethod
|
|
def _read_int_8(data: bytes, offset: int = 0) -> int:
|
|
return int.from_bytes(data[offset: offset + 1], byteorder="big", signed=True)
|
|
|
|
@staticmethod
|
|
def _read_int_8_le(data: bytes, offset: int = 0) -> int:
|
|
return int.from_bytes(
|
|
data[offset: offset + 1], byteorder="little", signed=True
|
|
)
|
|
|
|
@staticmethod
|
|
def _read_int_16(data: bytes, offset: int = 0) -> int:
|
|
return int.from_bytes(data[offset: offset + 2], byteorder="big", signed=True)
|
|
|
|
@staticmethod
|
|
def _read_int_16_le(data: bytes, offset: int = 0) -> int:
|
|
return int.from_bytes(
|
|
data[offset: offset + 2], byteorder="little", signed=True
|
|
)
|
|
|
|
@staticmethod
|
|
def _compare_segment_neighbors(r1: Segment, r2: Segment) -> bool:
|
|
alen = 0
|
|
blen = 0
|
|
if r1.neighbors:
|
|
alen = len(r1.neighbors)
|
|
if r2.neighbors:
|
|
blen = len(r2.neighbors)
|
|
|
|
if alen == blen:
|
|
return r1.segment_id - r2.segment_id
|
|
|
|
return blen - alen
|
|
|
|
@staticmethod
|
|
def _compare_colors(c1: list[int], c2: list[int]) -> bool:
|
|
return c1[1] - c2[1] if c1[1] != c2[1] else c1[0] - c2[0]
|
|
|
|
@staticmethod
|
|
def _get_pixel_type(map_data: MapData, pixel, vslam_map: bool = False) -> MapPixelType:
|
|
if map_data.frame_map:
|
|
segment_id = pixel >> 2
|
|
|
|
if 0 < segment_id < 64:
|
|
if segment_id == 63:
|
|
return MapPixelType.WALL.value
|
|
if segment_id == 62:
|
|
return MapPixelType.FLOOR.value
|
|
if segment_id == 61:
|
|
return MapPixelType.UNKNOWN.value
|
|
return segment_id
|
|
|
|
segment_id = pixel & 0x3f
|
|
# as implemented on the app
|
|
if segment_id == 1 or segment_id == 3:
|
|
return MapPixelType.NEW_SEGMENT.value
|
|
if segment_id == 2:
|
|
return MapPixelType.WALL.value
|
|
elif vslam_map:
|
|
segment_id = pixel & 0b01111111
|
|
if segment_id == 1:
|
|
return MapPixelType.NEW_SEGMENT.value
|
|
elif segment_id == 3:
|
|
return MapPixelType.NEW_SEGMENT_UNKNOWN.value
|
|
elif segment_id == 2:
|
|
return MapPixelType.WALL.value
|
|
else:
|
|
if pixel >> 7:
|
|
return MapPixelType.WALL.value
|
|
|
|
segment_id = pixel & 0x3f
|
|
if segment_id > 0:
|
|
if map_data.saved_map_status == 1 or map_data.saved_map_status == 0:
|
|
# as implemented on the app
|
|
if segment_id == 1 or segment_id == 3:
|
|
return MapPixelType.NEW_SEGMENT.value
|
|
if segment_id == 2:
|
|
return MapPixelType.WALL.value
|
|
return MapPixelType.OUTSIDE.value
|
|
|
|
return segment_id
|
|
|
|
return MapPixelType.OUTSIDE.value
|
|
|
|
@staticmethod
|
|
def _get_segment_center(map_data, segment_id: int, center: int, vertical: bool) -> int | None:
|
|
# Find center point implemented as on the app
|
|
lines = []
|
|
zero_pixels = -1
|
|
segment_pixel = 0
|
|
line = None
|
|
|
|
for k in range(
|
|
map_data.dimensions.height if vertical else map_data.dimensions.width
|
|
):
|
|
pixel_type = (
|
|
map_data.data[
|
|
(k * map_data.dimensions.width + center)
|
|
if vertical
|
|
else (center * map_data.dimensions.width + k)
|
|
]
|
|
& 0x3F
|
|
)
|
|
if pixel_type == segment_id:
|
|
segment_pixel = k
|
|
zero_pixels = 0
|
|
if line is None:
|
|
line = [segment_pixel]
|
|
elif pixel_type == 0:
|
|
if zero_pixels >= 0:
|
|
zero_pixels = zero_pixels + 1
|
|
if zero_pixels >= 4 and line is not None:
|
|
line.append(segment_pixel)
|
|
lines.append(line)
|
|
line = None
|
|
elif line is not None:
|
|
line.append(segment_pixel)
|
|
lines.append(line)
|
|
line = None
|
|
|
|
if line is not None:
|
|
line.append(segment_pixel)
|
|
lines.append(line)
|
|
line = None
|
|
|
|
if lines:
|
|
maxLine = lines[0]
|
|
if len(lines) > 1:
|
|
for item in lines[1:]:
|
|
if item[1] - item[0] > maxLine[1] - maxLine[0]:
|
|
maxLine = item
|
|
|
|
return int(math.ceil((maxLine[1] - maxLine[0]) / 2 + maxLine[0]))
|
|
return None
|
|
|
|
|
|
@staticmethod
|
|
def decode_map_partial(raw_map, iv=None, key=None) -> MapDataPartial | None:
|
|
_LOGGER.debug("raw_map: %s", raw_map)
|
|
raw_map = raw_map.replace("_", "/").replace("-", "+")
|
|
|
|
if "," in raw_map and key is None:
|
|
values = raw_map.split(",")
|
|
key = values[1]
|
|
raw_map = values[0]
|
|
|
|
raw_map = base64.decodebytes(raw_map.encode("utf8"))
|
|
|
|
if key is not None:
|
|
if iv is None:
|
|
iv = ""
|
|
try:
|
|
key = hashlib.sha256(key.encode()).hexdigest()[
|
|
0:32].encode('utf8')
|
|
cipher = Cipher(algorithms.AES(key), modes.CBC(
|
|
iv.encode("utf8")), backend=default_backend())
|
|
decryptor = cipher.decryptor()
|
|
raw_map = decryptor.update(raw_map) + decryptor.finalize()
|
|
except Exception as ex:
|
|
_LOGGER.error(f"Map data decryption failed: {ex}. Private key might be missing, please report this issue with your device model https://github.com/Tasshack/dreame-vacuum/issues/new?assignees=Tasshack&labels=bug&template=bug_report.md&title=Map%20data%20decryption%20failed")
|
|
return None
|
|
|
|
try:
|
|
raw_map = zlib.decompress(raw_map)
|
|
if not raw_map or len(raw_map) < DreameVacuumMapDecoder.HEADER_SIZE:
|
|
_LOGGER.error("Wrong header size for map")
|
|
return None
|
|
except Exception as ex:
|
|
_LOGGER.error("Map data decompression failed: %s", ex)
|
|
return None
|
|
|
|
partial_map = MapDataPartial()
|
|
partial_map.map_id = DreameVacuumMapDecoder._read_int_16_le(raw_map)
|
|
partial_map.frame_id = DreameVacuumMapDecoder._read_int_16_le(
|
|
raw_map, 2)
|
|
partial_map.frame_type = DreameVacuumMapDecoder._read_int_8(raw_map, 4)
|
|
partial_map.raw = raw_map
|
|
image_size = DreameVacuumMapDecoder.HEADER_SIZE + (
|
|
DreameVacuumMapDecoder._read_int_16_le(raw_map, 19)
|
|
* DreameVacuumMapDecoder._read_int_16_le(raw_map, 21)
|
|
)
|
|
if len(raw_map) >= image_size:
|
|
try:
|
|
data_json = json.loads(raw_map[image_size:].decode("utf8"))
|
|
if data_json.get("timestamp_ms"):
|
|
partial_map.timestamp_ms = int(data_json["timestamp_ms"])
|
|
|
|
partial_map.data_json = data_json
|
|
except:
|
|
pass
|
|
return partial_map
|
|
|
|
@staticmethod
|
|
def decode_map(raw_map: str, vslam_map: bool, rotation: int = 0, iv: str = None, key: str = None) -> Tuple[MapData, Optional[MapData]]:
|
|
return DreameVacuumMapDecoder.decode_map_data_from_partial(
|
|
DreameVacuumMapDecoder.decode_map_partial(raw_map, iv, key), vslam_map, rotation
|
|
)
|
|
|
|
@staticmethod
|
|
def decode_saved_map(raw_map: str, vslam_map: bool, rotation: int = 0, iv: str = None) -> MapData | None:
|
|
return DreameVacuumMapDecoder.decode_map(raw_map, vslam_map, rotation, iv)[0]
|
|
|
|
@staticmethod
|
|
def decode_map_data_from_partial(
|
|
partial_map: MapDataPartial, vslam_map: bool, rotation: int = 0
|
|
) -> MapData | None:
|
|
if partial_map is None:
|
|
return
|
|
|
|
map_data = MapData()
|
|
map_data.map_id = partial_map.map_id
|
|
map_data.frame_id = partial_map.frame_id
|
|
map_data.frame_type = partial_map.frame_type
|
|
map_data.timestamp_ms = partial_map.timestamp_ms
|
|
|
|
raw = partial_map.raw
|
|
map_data.robot_position = Point(
|
|
DreameVacuumMapDecoder._read_int_16_le(raw, 5),
|
|
DreameVacuumMapDecoder._read_int_16_le(raw, 7),
|
|
DreameVacuumMapDecoder._read_int_16_le(raw, 9),
|
|
)
|
|
map_data.charger_position = Point(
|
|
DreameVacuumMapDecoder._read_int_16_le(raw, 11),
|
|
DreameVacuumMapDecoder._read_int_16_le(raw, 13),
|
|
DreameVacuumMapDecoder._read_int_16_le(raw, 15),
|
|
)
|
|
|
|
grid_size = DreameVacuumMapDecoder._read_int_16_le(raw, 17)
|
|
width = DreameVacuumMapDecoder._read_int_16_le(raw, 19)
|
|
height = DreameVacuumMapDecoder._read_int_16_le(raw, 21)
|
|
left = DreameVacuumMapDecoder._read_int_16_le(raw, 23)
|
|
top = DreameVacuumMapDecoder._read_int_16_le(raw, 25)
|
|
|
|
image_size = DreameVacuumMapDecoder.HEADER_SIZE + width * height
|
|
map_data.dimensions = MapImageDimensions(
|
|
top, left, height, width, grid_size)
|
|
data_json = partial_map.data_json
|
|
if data_json is None:
|
|
data_json = {}
|
|
|
|
_LOGGER.debug("Map Data Json: %s", data_json)
|
|
|
|
map_data.rotation = rotation
|
|
if "mra" in data_json:
|
|
map_data.rotation = int(data_json["mra"])
|
|
|
|
if "robot_mode" in data_json:
|
|
map_data.robot_mode = int(data_json["robot_mode"])
|
|
|
|
if "map_used_times" in data_json:
|
|
map_data.used_times = int(data_json["map_used_times"])
|
|
|
|
if "cs" in data_json:
|
|
map_data.cleaned_area = int(data_json["cs"])
|
|
|
|
map_data.docked = bool(data_json.get("oc") and data_json["oc"])
|
|
|
|
map_data.l2r = bool(data_json.get("l2r") and data_json["l2r"])
|
|
map_data.frame_map = bool(data_json.get(
|
|
"fsm") and data_json["fsm"] == 1)
|
|
map_data.restored_map = bool(
|
|
data_json.get("rpur") and data_json["rpur"] == 1)
|
|
map_data.saved_map_status = -1
|
|
if "ris" in data_json:
|
|
map_data.saved_map_status = data_json["ris"]
|
|
map_data.clean_log = bool(
|
|
data_json.get("iscleanlog") and data_json["iscleanlog"] == True
|
|
)
|
|
map_data.recovery_map = bool(
|
|
data_json.get("us") and data_json["us"] == 1)
|
|
map_data.new_map = bool("risp" in data_json and data_json["risp"] == 0)
|
|
|
|
map_data.temporary_map = bool(
|
|
data_json.get("suw")
|
|
and (data_json["suw"] == 6 or data_json["suw"] == 5)
|
|
and data_json.get("fsm") is None
|
|
)
|
|
map_data.saved_map = bool(
|
|
map_data.frame_type == MapFrameType.I.value
|
|
and not map_data.restored_map
|
|
and not map_data.frame_map
|
|
and map_data.saved_map_status == -1
|
|
and not map_data.clean_log
|
|
)
|
|
|
|
if (
|
|
data_json.get("nc") and data_json["nc"]
|
|
) or map_data.charger_position.a == 32767:
|
|
map_data.charger_position = None
|
|
|
|
if (
|
|
(data_json.get("nr") and data_json["nr"])
|
|
or map_data.robot_position.a == 32767
|
|
):
|
|
map_data.robot_position = None
|
|
|
|
if not map_data.saved_map and not map_data.recovery_map:
|
|
map_data.index = 0
|
|
|
|
if data_json.get("tr"):
|
|
matches = [
|
|
m.groupdict()
|
|
for m in re.compile(
|
|
r"(?P<operator>[MWSLl])(?P<x>-?\d+),(?P<y>-?\d+)"
|
|
).finditer(data_json["tr"])
|
|
]
|
|
current_position = Point(0, 0)
|
|
map_data.path = []
|
|
for match in matches:
|
|
operator = match["operator"]
|
|
x = int(match["x"])
|
|
y = int(match["y"])
|
|
|
|
if operator == "L":
|
|
current_position = Path(
|
|
current_position.x + x,
|
|
current_position.y + y,
|
|
PathType.LINE
|
|
)
|
|
else:
|
|
# You will only get "l" paths with in a P frame.
|
|
# It means path is connected with the path from previous frame and it should be rendered as a line.
|
|
if operator == "l":
|
|
operator = "L"
|
|
current_position = Path(x, y, PathType(operator))
|
|
|
|
map_data.path.append(current_position)
|
|
|
|
if data_json.get("sa") and isinstance(data_json["sa"], list):
|
|
map_data.active_segments = [sa[0] for sa in data_json["sa"]]
|
|
|
|
if data_json.get("da2"):
|
|
if data_json["da2"].get("areas"):
|
|
map_data.active_areas = []
|
|
for area in data_json["da2"]["areas"]:
|
|
x_coords = sorted([area[0], area[2]])
|
|
y_coords = sorted([area[1], area[3]])
|
|
map_data.active_areas.append(
|
|
Area(
|
|
x_coords[0],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[1],
|
|
x_coords[0],
|
|
y_coords[1],
|
|
)
|
|
)
|
|
|
|
if data_json.get("sp"):
|
|
map_data.active_points = []
|
|
for point in data_json["sp"]:
|
|
map_data.active_points.append(Point(point[0], point[1]))
|
|
|
|
if "cleanset" in data_json:
|
|
map_data.cleanset = data_json["cleanset"]
|
|
if isinstance(map_data.cleanset, str):
|
|
map_data.cleanset = json.loads(map_data.cleanset)
|
|
|
|
if "ai_obstacle" in data_json:
|
|
map_data.obstacles = []
|
|
for obstacle in data_json["ai_obstacle"]:
|
|
if len(obstacle) >= 4:
|
|
obstacle_type = int(obstacle[2])
|
|
if obstacle_type in ObstacleType._value2member_map_:
|
|
if len(obstacle) >= 7 and int(obstacle[4]) >= 1000:
|
|
map_data.obstacles.append(Obstacle(float(obstacle[0]), float(obstacle[1]), ObstacleType(
|
|
obstacle_type), int(float(obstacle[3]) * 100), obstacle[4], obstacle[5], obstacle[6]))
|
|
else:
|
|
map_data.obstacles.append(Obstacle(float(obstacle[0]), float(
|
|
obstacle[1]), ObstacleType(obstacle_type), int(float(obstacle[3]) * 100)))
|
|
|
|
map_data.empty_map = map_data.frame_type == MapFrameType.I.value
|
|
if (width * height) > 0:
|
|
map_data.data = raw[DreameVacuumMapDecoder.HEADER_SIZE:image_size]
|
|
map_data.empty_map = bool(width == 2 and height == 2)
|
|
if map_data.empty_map:
|
|
for y in range(height):
|
|
for x in range(width):
|
|
if map_data.data[(width * y) + x] > 0:
|
|
map_data.empty_map = False
|
|
break
|
|
|
|
map_data.pixel_type = np.full(
|
|
(width, height), MapPixelType.OUTSIDE.value, dtype=np.uint8)
|
|
if not map_data.empty_map:
|
|
if map_data.frame_type == MapFrameType.I.value:
|
|
if map_data.frame_map:
|
|
for y in range(height):
|
|
for x in range(width):
|
|
pixel = map_data.data[(width * y) + x]
|
|
if pixel > 0:
|
|
map_data.empty_map = False
|
|
segment_id = pixel >> 2
|
|
if 0 < segment_id < 64:
|
|
if segment_id == 63:
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.WALL.value
|
|
elif segment_id == 62:
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.FLOOR.value
|
|
elif segment_id == 61:
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.UNKNOWN.value
|
|
else:
|
|
map_data.pixel_type[x,
|
|
y] = segment_id
|
|
else:
|
|
segment_id = pixel & 0x3f
|
|
if segment_id == 1 or segment_id == 3:
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.NEW_SEGMENT.value
|
|
elif segment_id == 2:
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.WALL.value
|
|
elif (
|
|
map_data.saved_map_status == 1
|
|
or map_data.saved_map_status == 0
|
|
):
|
|
for y in range(height):
|
|
for x in range(width):
|
|
segment_id = map_data.data[(
|
|
width * y) + x] & 0x3f
|
|
# as implemented on the app
|
|
if segment_id == 1 or segment_id == 3:
|
|
map_data.empty_map = False
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.NEW_SEGMENT.value
|
|
elif segment_id == 2:
|
|
map_data.empty_map = False
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.WALL.value
|
|
elif vslam_map and not map_data.saved_map:
|
|
for y in range(height):
|
|
for x in range(width):
|
|
segment_id = map_data.data[(width * y) + x] & 0b00000011
|
|
if segment_id == 1:
|
|
map_data.empty_map = False
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.NEW_SEGMENT.value
|
|
elif segment_id == 3:
|
|
map_data.empty_map = False
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.NEW_SEGMENT_UNKNOWN.value
|
|
elif segment_id == 2:
|
|
map_data.empty_map = False
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.WALL.value
|
|
else:
|
|
for y in range(height):
|
|
for x in range(width):
|
|
pixel = map_data.data[(width * y) + x]
|
|
if pixel > 0:
|
|
map_data.empty_map = False
|
|
if pixel >> 7:
|
|
map_data.pixel_type[x,
|
|
y] = MapPixelType.WALL.value
|
|
else:
|
|
segment_id = pixel & 0x3f
|
|
if segment_id > 0:
|
|
map_data.pixel_type[x,
|
|
y] = segment_id
|
|
|
|
segments = DreameVacuumMapDecoder.get_segments(map_data, vslam_map)
|
|
if segments and data_json.get("seg_inf"):
|
|
seg_inf = data_json["seg_inf"]
|
|
for (k, v) in segments.items():
|
|
if seg_inf.get(str(k)):
|
|
segment_info = seg_inf[str(k)]
|
|
if segment_info.get("nei_id"):
|
|
segments[k].neighbors = segment_info["nei_id"]
|
|
if segment_info.get("type"):
|
|
segments[k].type = segment_info["type"]
|
|
if segment_info.get("index"):
|
|
segments[k].index = segment_info["index"]
|
|
if segment_info.get("roomID"):
|
|
segments[k].unique_id = segment_info["roomID"]
|
|
if segment_info.get(MAP_PARAMETER_NAME):
|
|
segments[k].custom_name = base64.b64decode(
|
|
segment_info.get(MAP_PARAMETER_NAME)
|
|
).decode("utf-8")
|
|
segments[k].set_name()
|
|
|
|
map_data.segments = segments
|
|
|
|
saved_map_data = None
|
|
restored_map = map_data.restored_map
|
|
|
|
if data_json.get("rism"):
|
|
saved_map_data = DreameVacuumMapDecoder.decode_saved_map(
|
|
data_json["rism"],
|
|
vslam_map,
|
|
map_data.rotation,
|
|
)
|
|
|
|
if saved_map_data is not None:
|
|
saved_map_data.timestamp_ms = map_data.timestamp_ms
|
|
map_data.saved_map_id = saved_map_data.map_id
|
|
if saved_map_data.temporary_map:
|
|
map_data.temporary_map = saved_map_data.temporary_map
|
|
|
|
if restored_map or map_data.recovery_map or (map_data.saved_map_status == 2 and map_data.empty_map):
|
|
map_data.segments = copy.deepcopy(saved_map_data.segments)
|
|
map_data.data = saved_map_data.data
|
|
map_data.pixel_type = saved_map_data.pixel_type
|
|
map_data.dimensions = saved_map_data.dimensions
|
|
|
|
if map_data.empty_map:
|
|
map_data.restored_map = False
|
|
restored_map = True
|
|
map_data.empty_map = False
|
|
else:
|
|
if saved_map_data.segments is not None:
|
|
if map_data.segments is None and (
|
|
map_data.saved_map_status == 1
|
|
or map_data.saved_map_status == 0
|
|
):
|
|
map_data.segments = {}
|
|
|
|
for (k, v) in saved_map_data.segments.items():
|
|
if map_data.segments and k in map_data.segments:
|
|
# as implemented on the app
|
|
map_data.segments[k].icon = v.icon
|
|
map_data.segments[k].name = v.name
|
|
map_data.segments[k].custom_name = v.custom_name
|
|
map_data.segments[k].type = v.type
|
|
map_data.segments[k].index = v.index
|
|
map_data.segments[k].unique_id = v.unique_id
|
|
map_data.segments[k].neighbors = v.neighbors
|
|
if map_data.saved_map_status == 2:
|
|
map_data.segments[k].x = v.x
|
|
map_data.segments[k].y = v.y
|
|
|
|
if not saved_map_data.cleanset:
|
|
saved_map_data.cleanset = copy.deepcopy(map_data.cleanset)
|
|
|
|
if (
|
|
(map_data.saved_map_status == 2 or map_data.docked)
|
|
and map_data.charger_position is None
|
|
and not map_data.saved_map
|
|
and saved_map_data.charger_position
|
|
):
|
|
map_data.charger_position = saved_map_data.charger_position
|
|
|
|
if map_data.saved_map_status == 2:
|
|
map_data.no_go_areas = saved_map_data.no_go_areas
|
|
map_data.no_mopping_areas = saved_map_data.no_mopping_areas
|
|
map_data.walls = saved_map_data.walls
|
|
|
|
if vslam_map:
|
|
map_data.segments = copy.deepcopy(saved_map_data.segments)
|
|
map_data.charger_position = copy.deepcopy(saved_map_data.charger_position)
|
|
|
|
if (
|
|
not map_data.saved_map
|
|
and map_data.robot_position is None
|
|
and map_data.docked
|
|
and map_data.charger_position
|
|
):
|
|
map_data.robot_position = copy.deepcopy(map_data.charger_position)
|
|
|
|
if map_data.segments:
|
|
if not map_data.saved_map:
|
|
DreameVacuumMapDecoder.set_segment_cleanset(
|
|
map_data, map_data.cleanset)
|
|
DreameVacuumMapDecoder.set_robot_segment(map_data)
|
|
|
|
if map_data.saved_map_status == 2 or map_data.saved_map:
|
|
DreameVacuumMapDecoder.set_segment_color_index(map_data)
|
|
|
|
if data_json.get("vw"):
|
|
if data_json["vw"].get("rect") and not map_data.no_go_areas:
|
|
map_data.no_go_areas = []
|
|
for area in data_json["vw"]["rect"]:
|
|
x_coords = sorted([area[0], area[2]])
|
|
y_coords = sorted([area[1], area[3]])
|
|
map_data.no_go_areas.append(
|
|
Area(
|
|
x_coords[0],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[1],
|
|
x_coords[0],
|
|
y_coords[1],
|
|
)
|
|
)
|
|
|
|
if data_json["vw"].get("mop") and not map_data.no_mopping_areas:
|
|
map_data.no_mopping_areas = []
|
|
for area in data_json["vw"]["mop"]:
|
|
x_coords = sorted([area[0], area[2]])
|
|
y_coords = sorted([area[1], area[3]])
|
|
map_data.no_mopping_areas.append(
|
|
Area(
|
|
x_coords[0],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[0],
|
|
x_coords[1],
|
|
y_coords[1],
|
|
x_coords[0],
|
|
y_coords[1],
|
|
)
|
|
)
|
|
|
|
if data_json["vw"].get("line") and not map_data.walls:
|
|
map_data.walls = [
|
|
Wall(
|
|
virtual_wall[0],
|
|
virtual_wall[1],
|
|
virtual_wall[2],
|
|
virtual_wall[3],
|
|
)
|
|
for virtual_wall in data_json["vw"]["line"]
|
|
]
|
|
|
|
if vslam_map and not map_data.saved_map:
|
|
map_data.need_optimization = not restored_map
|
|
|
|
return map_data, saved_map_data
|
|
|
|
@staticmethod
|
|
def decode_p_map_data_from_partial(
|
|
partial_map: MapDataPartial, current_map_data: MapData, vslam_map: bool
|
|
) -> MapData | None:
|
|
if partial_map.frame_type != MapFrameType.P.value:
|
|
return None
|
|
|
|
map_data, saved_map_data = DreameVacuumMapDecoder.decode_map_data_from_partial(
|
|
partial_map,
|
|
vslam_map,
|
|
)
|
|
if map_data is None:
|
|
return None
|
|
|
|
current_map_data.frame_id = map_data.frame_id
|
|
current_map_data.robot_position = map_data.robot_position
|
|
current_map_data.timestamp_ms = map_data.timestamp_ms
|
|
current_map_data.docked = map_data.docked
|
|
current_map_data.temporary_map = map_data.temporary_map
|
|
current_map_data.saved_map = False
|
|
current_map_data.empty_map = False
|
|
current_map_data.restored_map = False
|
|
current_map_data.recovery_map = False
|
|
current_map_data.clean_log = False
|
|
|
|
if map_data.charger_position is not None and (not vslam_map or current_map_data.saved_map_status != 2):
|
|
current_map_data.charger_position = map_data.charger_position
|
|
|
|
if map_data.obstacles is not None:
|
|
current_map_data.obstacles = map_data.obstacles
|
|
|
|
# P map only returns difference between its previous frame.
|
|
# Calculate new map size and update the buffer according to the received data at received offset.
|
|
if map_data.data:
|
|
current_dimensions = current_map_data.dimensions
|
|
new_dimensions = map_data.dimensions
|
|
|
|
# Find max image size
|
|
grid_size = new_dimensions.grid_size
|
|
left = min(new_dimensions.left, current_dimensions.left)
|
|
top = min(new_dimensions.top, current_dimensions.top)
|
|
max_left = max(
|
|
new_dimensions.left + (new_dimensions.width * grid_size),
|
|
current_dimensions.left
|
|
+ (current_dimensions.width * current_dimensions.grid_size),
|
|
)
|
|
max_top = max(
|
|
new_dimensions.top + (new_dimensions.height * grid_size),
|
|
current_dimensions.top
|
|
+ (current_dimensions.height * current_dimensions.grid_size),
|
|
)
|
|
|
|
# Calculate new image size
|
|
width = int((max_left - left) / grid_size)
|
|
height = int((max_top - top) / grid_size)
|
|
|
|
# Create new buffer
|
|
data = np.zeros((width * height), np.uint8)
|
|
pixel_type = np.full(
|
|
(width, height), MapPixelType.OUTSIDE.value, dtype=np.uint8)
|
|
|
|
# Calculate old image offset
|
|
left_offset = int(
|
|
(current_dimensions.left - left) / current_dimensions.grid_size
|
|
)
|
|
top_offset = int(
|
|
(current_dimensions.top - top) / current_dimensions.grid_size
|
|
)
|
|
|
|
# Copy old image to buffer
|
|
for y in range(current_dimensions.height):
|
|
for x in range(current_dimensions.width):
|
|
data[
|
|
(width * (top_offset + y)) + left_offset + x
|
|
] = current_map_data.data[(current_dimensions.width * y) + x]
|
|
pixel_type[
|
|
left_offset + x, top_offset + y
|
|
] = current_map_data.pixel_type[x, y]
|
|
|
|
# Calculate new image offset
|
|
left_offset = int((new_dimensions.left - left) / grid_size)
|
|
top_offset = int((new_dimensions.top - top) / grid_size)
|
|
|
|
# Copy new image to buffer at calculated offset
|
|
for y in range(new_dimensions.height):
|
|
for x in range(new_dimensions.width):
|
|
current_index = (new_dimensions.width * y) + x
|
|
if map_data.data[current_index]:
|
|
new_index = (width * (top_offset + y)) + \
|
|
left_offset + x
|
|
# Add current buffer value to new buffer value for finding the new pixel value
|
|
data[new_index] = data[new_index] + \
|
|
map_data.data[current_index]
|
|
# Calculate the new pixel type from updated buffer value
|
|
pixel_type[
|
|
left_offset + x, top_offset + y
|
|
] = DreameVacuumMapDecoder._get_pixel_type(
|
|
current_map_data, int(data[new_index]), vslam_map,
|
|
)
|
|
|
|
# Update size and buffer
|
|
current_map_data.data = bytes(data)
|
|
current_map_data.pixel_type = pixel_type
|
|
current_map_data.dimensions = MapImageDimensions(
|
|
top, left, height, width, grid_size
|
|
)
|
|
|
|
if vslam_map:
|
|
current_map_data.need_optimization = True
|
|
|
|
if map_data.path:
|
|
# Append new paths received with P frame
|
|
if current_map_data.path:
|
|
current_map_data.path.extend(map_data.path)
|
|
else:
|
|
current_map_data.path = map_data.path
|
|
|
|
DreameVacuumMapDecoder.set_robot_segment(current_map_data)
|
|
|
|
# if robotPos.l2r == True and self._robotPos.l2r == True:
|
|
# self._lastPos = self._robotPos
|
|
# else:
|
|
# self._lastPos = None
|
|
return current_map_data
|
|
|
|
@staticmethod
|
|
def get_segments(map_data: MapData, vslam_map: bool) -> dict[str, Any]:
|
|
segments = {}
|
|
for y in range(map_data.dimensions.height):
|
|
for x in range(map_data.dimensions.width):
|
|
segment_id = int(map_data.pixel_type[x, y])
|
|
if segment_id > 0 and segment_id < 64:
|
|
if segment_id not in segments:
|
|
segments[segment_id] = Segment(segment_id, x, y, x, y)
|
|
continue
|
|
|
|
if x < segments[segment_id].x0:
|
|
segments[segment_id].x0 = x
|
|
elif x > segments[segment_id].x1:
|
|
segments[segment_id].x1 = x
|
|
|
|
if y < segments[segment_id].y0:
|
|
segments[segment_id].y0 = y
|
|
elif y > segments[segment_id].y1:
|
|
segments[segment_id].y1 = y
|
|
|
|
if segments:
|
|
for (k, v) in segments.items():
|
|
x = int(math.ceil((v.x1 - v.x0) / 2 + v.x0))
|
|
y = int(math.ceil((v.y1 - v.y0) / 2 + v.y0))
|
|
|
|
if map_data.saved_map:
|
|
if vslam_map:
|
|
if map_data.pixel_type[x, y] != k:
|
|
startI = -1
|
|
endI = -1
|
|
for i in range(map_data.dimensions.width):
|
|
value = map_data.pixel_type[i, y]
|
|
if startI == -1:
|
|
if value == k:
|
|
startI = i
|
|
elif value != k or i == (map_data.dimensions.width - 1):
|
|
endI = (i - 1)
|
|
break
|
|
|
|
if startI != -1 and endI != -1:
|
|
x = (endI - startI) + startI
|
|
else:
|
|
center_x = DreameVacuumMapDecoder._get_segment_center(
|
|
map_data, k, y, False
|
|
)
|
|
if center_x is not None:
|
|
center_y = DreameVacuumMapDecoder._get_segment_center(
|
|
map_data, k, center_x, True
|
|
)
|
|
if center_y is not None:
|
|
x = center_x
|
|
y = center_y
|
|
|
|
segments[k].x0 = (
|
|
int(
|
|
map_data.dimensions.left
|
|
+ (v.x0 * map_data.dimensions.grid_size)
|
|
)
|
|
)
|
|
segments[k].y0 = (
|
|
int(
|
|
map_data.dimensions.top +
|
|
(v.y0 * map_data.dimensions.grid_size)
|
|
- map_data.dimensions.grid_size
|
|
)
|
|
)
|
|
segments[k].x1 = (
|
|
int(
|
|
map_data.dimensions.left
|
|
+ (v.x1 * map_data.dimensions.grid_size)
|
|
+ map_data.dimensions.grid_size
|
|
)
|
|
)
|
|
segments[k].y1 = (
|
|
int(
|
|
map_data.dimensions.top +
|
|
(v.y1 * map_data.dimensions.grid_size)
|
|
)
|
|
)
|
|
segments[k].x = int(
|
|
map_data.dimensions.left +
|
|
(x * map_data.dimensions.grid_size)
|
|
)
|
|
segments[k].y = int(
|
|
map_data.dimensions.top +
|
|
(y * map_data.dimensions.grid_size)
|
|
)
|
|
segments[k].set_name()
|
|
return segments
|
|
|
|
@staticmethod
|
|
def set_robot_segment(map_data: MapData) -> None:
|
|
if (
|
|
map_data.segments and
|
|
map_data.saved_map_status == 2
|
|
and map_data.robot_position is not None
|
|
):
|
|
map_data.robot_segment = map_data.pixel_type[
|
|
int((map_data.robot_position.x - map_data.dimensions.left) /
|
|
map_data.dimensions.grid_size),
|
|
int((map_data.robot_position.y - map_data.dimensions.top) /
|
|
map_data.dimensions.grid_size),
|
|
]
|
|
if map_data.robot_segment not in map_data.segments:
|
|
map_data.robot_segment = 0
|
|
else:
|
|
map_data.robot_segment = None
|
|
|
|
@staticmethod
|
|
def set_segment_cleanset(map_data: MapData, cleanset: dict[str, list[int]]) -> None:
|
|
if map_data is not None and map_data.segments is not None:
|
|
for (k, v) in map_data.segments.items():
|
|
if cleanset is not None:
|
|
segment_id = str(k)
|
|
if segment_id not in cleanset:
|
|
cleanset[segment_id] = [
|
|
1,
|
|
3,
|
|
1,
|
|
0,
|
|
] # Cleanset returns empty on restored map but robot uses these default values when that happens
|
|
if map_data.segments[k].cleaning_mode is not None:
|
|
cleanset[segment_id].append(2)
|
|
|
|
item = cleanset[segment_id]
|
|
|
|
map_data.segments[k].suction_level = item[0]
|
|
map_data.segments[k].water_volume = (
|
|
item[1] - 1
|
|
) # for some reason cleanset uses different int values for water volume
|
|
map_data.segments[k].cleaning_times = item[2]
|
|
map_data.segments[k].order = item[3]
|
|
map_data.segments[k].cleaning_mode = item[4] if len(
|
|
item) > 4 else None
|
|
else:
|
|
map_data.segments[k].suction_level = None
|
|
map_data.segments[k].water_volume = None
|
|
map_data.segments[k].cleaning_times = None
|
|
map_data.segments[k].order = None
|
|
map_data.segments[k].cleaning_mode = None
|
|
|
|
@staticmethod
|
|
def set_segment_color_index(map_data: MapData) -> None:
|
|
"""Find segment color index as implemented on the app"""
|
|
area_color_index = {}
|
|
sorted_segments = sorted(
|
|
map_data.segments.values(),
|
|
key=cmp_to_key(DreameVacuumMapDecoder._compare_segment_neighbors),
|
|
)
|
|
for segment in sorted_segments:
|
|
used_ids = []
|
|
if segment.neighbors is not None:
|
|
for nid in segment.neighbors:
|
|
if nid in area_color_index:
|
|
used_ids.append(area_color_index[nid])
|
|
|
|
area_color_num = {}
|
|
for i in range(4):
|
|
area_color_num[i] = [i, 0]
|
|
|
|
for (i, j) in area_color_index.items():
|
|
area_color_num[j][1] = area_color_num[j][1] + 1
|
|
|
|
area_color_num = sorted(
|
|
area_color_num.values(),
|
|
key=cmp_to_key(DreameVacuumMapDecoder._compare_colors),
|
|
)
|
|
|
|
for area_color in area_color_num:
|
|
color = area_color[0]
|
|
if color not in used_ids:
|
|
area_color_index[segment.segment_id] = color
|
|
break
|
|
|
|
if segment.segment_id not in area_color_index:
|
|
area_color_index[segment.segment_id] = 0
|
|
|
|
for i in area_color_index:
|
|
map_data.segments[i].color_index = area_color_index[i]
|
|
|
|
|
|
class DreameVacuumMapDataRenderer:
|
|
HALF_INT16 = 32768
|
|
HALF_INT16_UPPER_HALF = 32767
|
|
MAX = round(((HALF_INT16 + HALF_INT16_UPPER_HALF) / 10))
|
|
|
|
def __init__(self) -> None:
|
|
self._map_data: MapData = None
|
|
self._map_data_json: dict[str, Any] = None
|
|
self._left: int = 0
|
|
self._top: int = 0
|
|
self._grid_size: int = 0
|
|
self.render_complete: bool = True
|
|
self._layers: dict[MapRendererLayer, dict[str, Any]] = {}
|
|
|
|
self._default_map_data: str = base64.b64decode(DEFAULT_MAP_DATA)
|
|
self._default_map_image = Image.open(
|
|
BytesIO(base64.b64decode(DEFAULT_MAP_DATA_IMAGE))
|
|
).convert("RGBA")
|
|
|
|
@staticmethod
|
|
def _coordinate_tuple_sort(a: list[int], b: list[int]) -> bool:
|
|
xA = a[0]
|
|
yA = a[1]
|
|
xB = b[0]
|
|
yB = b[1]
|
|
|
|
if yB > yA:
|
|
return -1
|
|
if xB > xA:
|
|
return 1
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _convert_coordinates(x: int, y: int) -> int:
|
|
return [
|
|
round((x + DreameVacuumMapDataRenderer.HALF_INT16) / 10),
|
|
DreameVacuumMapDataRenderer.MAX
|
|
- round((y + DreameVacuumMapDataRenderer.HALF_INT16) / 10),
|
|
]
|
|
|
|
@staticmethod
|
|
def _convert_angle(angle: int) -> int:
|
|
return (((180 - angle) if (angle < 180) else (360 - angle + 180)) + 270) % 360
|
|
|
|
@staticmethod
|
|
def _to_buffer(image, extra_data: str) -> bytes:
|
|
buffer = io.BytesIO()
|
|
info = PngImagePlugin.PngInfo()
|
|
info.add_text("ValetudoMap", extra_data, zip=True)
|
|
image.save(buffer, format="PNG", pnginfo=info)
|
|
return buffer.getvalue()
|
|
|
|
def render_map(self, map_data: MapData, robot_status: int = 0) -> bytes:
|
|
if map_data is None or map_data.empty_map:
|
|
return self.default_map_image
|
|
|
|
if (
|
|
self._map_data
|
|
and self._map_data == map_data
|
|
and self._map_data.segments == map_data.segments
|
|
and self._map_data.frame_id == map_data.frame_id
|
|
and self._map_data_json
|
|
):
|
|
_LOGGER.debug("Skip render map data, not changed")
|
|
return self._to_buffer(
|
|
self._default_map_image,
|
|
json.dumps(self._map_data_json, separators=(",", ":")),
|
|
)
|
|
|
|
now = time.time()
|
|
self.render_complete = False
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.dimensions != map_data.dimensions
|
|
or self._map_data.map_id != map_data.map_id
|
|
or self._map_data.saved_map_status != map_data.saved_map_status
|
|
):
|
|
self._map_data = None
|
|
self._left = round(
|
|
(map_data.dimensions.left +
|
|
DreameVacuumMapDataRenderer.HALF_INT16) / 10
|
|
)
|
|
self._top = round(
|
|
(map_data.dimensions.top + DreameVacuumMapDataRenderer.HALF_INT16) / 10
|
|
)
|
|
self._grid_size = round(map_data.dimensions.grid_size / 10)
|
|
|
|
map_data_json = {
|
|
MAP_DATA_PARAMETER_CLASS: "ValetudoMap",
|
|
MAP_DATA_PARAMETER_SIZE: {
|
|
MAP_DATA_PARAMETER_X: DreameVacuumMapDataRenderer.MAX,
|
|
MAP_DATA_PARAMETER_Y: DreameVacuumMapDataRenderer.MAX,
|
|
},
|
|
MAP_DATA_PARAMETER_PIXEL_SIZE: self._grid_size,
|
|
MAP_DATA_PARAMETER_LAYERS: [],
|
|
MAP_DATA_PARAMETER_ENTITIES: [],
|
|
MAP_DATA_PARAMETER_META_DATA: {MAP_DATA_PARAMETER_VERSION: 2, MAP_DATA_PARAMETER_ROTATION: map_data.rotation},
|
|
}
|
|
|
|
if map_data.robot_position:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.robot_position != map_data.robot_position
|
|
or not self._layers.get(MapRendererLayer.ROBOT)
|
|
):
|
|
self._layers[MapRendererLayer.ROBOT] = {
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_ROBOT_POSITION,
|
|
MAP_DATA_PARAMETER_POINTS: DreameVacuumMapDataRenderer._convert_coordinates(
|
|
map_data.robot_position.x, map_data.robot_position.y
|
|
),
|
|
MAP_DATA_PARAMETER_META_DATA: {
|
|
MAP_PARAMETER_ANGLE: DreameVacuumMapDataRenderer._convert_angle(
|
|
map_data.robot_position.a
|
|
)
|
|
},
|
|
}
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].append(
|
|
self._layers[MapRendererLayer.ROBOT])
|
|
|
|
if map_data.charger_position:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.charger_position != map_data.charger_position
|
|
or not self._layers.get(MapRendererLayer.CHARGER)
|
|
):
|
|
self._layers[MapRendererLayer.CHARGER] = {
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_CHARGER_POSITION,
|
|
MAP_DATA_PARAMETER_POINTS: DreameVacuumMapDataRenderer._convert_coordinates(
|
|
map_data.charger_position.x, map_data.charger_position.y
|
|
),
|
|
MAP_DATA_PARAMETER_META_DATA: {
|
|
MAP_PARAMETER_ANGLE: DreameVacuumMapDataRenderer._convert_angle(
|
|
map_data.charger_position.a
|
|
)
|
|
},
|
|
}
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].append(
|
|
self._layers[MapRendererLayer.CHARGER])
|
|
|
|
if map_data.no_mopping_areas:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.no_mopping_areas != map_data.no_mopping_areas
|
|
or not self._layers.get(MapRendererLayer.NO_MOP)
|
|
):
|
|
self._layers[MapRendererLayer.NO_MOP] = []
|
|
for area in map_data.no_mopping_areas:
|
|
a = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x0, area.y0
|
|
)
|
|
b = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x1, area.y1
|
|
)
|
|
c = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x2, area.y2
|
|
)
|
|
d = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x3, area.y3
|
|
)
|
|
self._layers[MapRendererLayer.NO_MOP].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_NO_MOP_AREA,
|
|
MAP_DATA_PARAMETER_POINTS: [a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]],
|
|
}
|
|
)
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].extend(
|
|
self._layers[MapRendererLayer.NO_MOP])
|
|
|
|
if map_data.no_go_areas:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.no_go_areas != map_data.no_go_areas
|
|
or not self._layers.get(MapRendererLayer.NO_GO)
|
|
):
|
|
self._layers[MapRendererLayer.NO_GO] = []
|
|
for area in map_data.no_go_areas:
|
|
a = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x0, area.y0
|
|
)
|
|
b = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x1, area.y1
|
|
)
|
|
c = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x2, area.y2
|
|
)
|
|
d = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x3, area.y3
|
|
)
|
|
|
|
self._layers[MapRendererLayer.NO_GO].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_NO_GO_AREA,
|
|
MAP_DATA_PARAMETER_POINTS: [a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]],
|
|
}
|
|
)
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].extend(
|
|
self._layers[MapRendererLayer.NO_GO])
|
|
|
|
if map_data.active_areas:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.active_areas != map_data.active_areas
|
|
or not self._layers.get(MapRendererLayer.ACTIVE_AREA)
|
|
):
|
|
self._layers[MapRendererLayer.ACTIVE_AREA] = []
|
|
for area in map_data.active_areas:
|
|
a = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x0, area.y0
|
|
)
|
|
b = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x1, area.y1
|
|
)
|
|
c = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x2, area.y2
|
|
)
|
|
d = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x3, area.y3
|
|
)
|
|
|
|
self._layers[MapRendererLayer.ACTIVE_AREA].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_ACTIVE_ZONE,
|
|
MAP_DATA_PARAMETER_POINTS: [a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]],
|
|
}
|
|
)
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].extend(
|
|
self._layers[MapRendererLayer.ACTIVE_AREA])
|
|
|
|
if map_data.active_points:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.active_points != map_data.active_points
|
|
or not self._layers.get(MapRendererLayer.ACTIVE_POINT)
|
|
):
|
|
self._layers[MapRendererLayer.ACTIVE_POINT] = []
|
|
size = 15 * map_data.dimensions.grid_size
|
|
for point in map_data.active_points:
|
|
area = Area(point.x - size, point.y - size, point.x + size, point.y -
|
|
size, point.x + size, point.y + size, point.x - size, point.y + size)
|
|
|
|
a = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x0, area.y0
|
|
)
|
|
b = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x1, area.y1
|
|
)
|
|
c = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x2, area.y2
|
|
)
|
|
d = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
area.x3, area.y3
|
|
)
|
|
|
|
self._layers[MapRendererLayer.ACTIVE_POINT].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_ACTIVE_ZONE,
|
|
MAP_DATA_PARAMETER_POINTS: [a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]],
|
|
}
|
|
)
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].extend(
|
|
self._layers[MapRendererLayer.ACTIVE_POINT])
|
|
|
|
if map_data.walls:
|
|
if self._map_data is None or self._map_data.walls != map_data.walls or not self._layers.get(MapRendererLayer.WALL):
|
|
self._layers[MapRendererLayer.WALL] = []
|
|
for wall in map_data.walls:
|
|
a = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
wall.x0, wall.y0
|
|
)
|
|
b = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
wall.x1, wall.y1
|
|
)
|
|
|
|
self._layers[MapRendererLayer.WALL].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_VIRTUAL_WALL,
|
|
MAP_DATA_PARAMETER_POINTS: [a[0], a[1], b[0], b[1]],
|
|
}
|
|
)
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].extend(
|
|
self._layers[MapRendererLayer.WALL])
|
|
|
|
if self._map_data is None or len(self._map_data.path) != len(map_data.path) or not self._layers.get(MapRendererLayer.PATH):
|
|
points = []
|
|
self._layers[MapRendererLayer.PATH] = []
|
|
if map_data.path and len(map_data.path) > 1:
|
|
s = map_data.path[0]
|
|
for point in map_data.path[1:]:
|
|
if point.path_type == PathType.LINE:
|
|
point = point
|
|
a = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
s.x, s.y)
|
|
b = DreameVacuumMapDataRenderer._convert_coordinates(
|
|
point.x, point.y
|
|
)
|
|
|
|
points.extend([a[0], a[1], b[0], b[1]])
|
|
else:
|
|
self._layers[MapRendererLayer.PATH].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_PATH,
|
|
MAP_DATA_PARAMETER_POINTS: points,
|
|
}
|
|
)
|
|
points = []
|
|
s = point
|
|
self._layers[MapRendererLayer.PATH].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_PATH,
|
|
MAP_DATA_PARAMETER_POINTS: points,
|
|
}
|
|
)
|
|
map_data_json[MAP_DATA_PARAMETER_ENTITIES].extend(
|
|
self._layers[MapRendererLayer.PATH])
|
|
|
|
floor_pixels = []
|
|
wall_pixels = []
|
|
segments = {}
|
|
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.active_segments != map_data.active_segments
|
|
or self._map_data.active_areas != map_data.active_areas
|
|
or self._map_data.segments != map_data.segments
|
|
or self._map_data.data != map_data.data
|
|
or not self._layers.get(MapRendererLayer.IMAGE)
|
|
):
|
|
self._layers[MapRendererLayer.IMAGE] = []
|
|
for y in range(map_data.dimensions.height):
|
|
for x in range(map_data.dimensions.width):
|
|
segment_id = int(map_data.pixel_type[x, y])
|
|
coords = [
|
|
(x + (self._left / self._grid_size)),
|
|
(y + (self._top / self._grid_size)),
|
|
]
|
|
|
|
coords[1] = (
|
|
DreameVacuumMapDataRenderer.MAX / self._grid_size
|
|
) - coords[1]
|
|
|
|
coords[0] = round(coords[0])
|
|
coords[1] = round(coords[1])
|
|
|
|
if segment_id == MapPixelType.WALL.value:
|
|
wall_pixels.append(coords)
|
|
elif segment_id == MapPixelType.FLOOR.value or segment_id == MapPixelType.UNKNOWN.value:
|
|
floor_pixels.append(coords)
|
|
elif segment_id > 0 and segment_id < 61:
|
|
if (
|
|
map_data.active_segments
|
|
and segment_id not in map_data.active_segments
|
|
):
|
|
floor_pixels.append(coords)
|
|
else:
|
|
if not map_data.segments:
|
|
segment_id = 1
|
|
|
|
if segment_id not in segments:
|
|
segments[segment_id] = []
|
|
segments[segment_id].append(coords)
|
|
|
|
if floor_pixels:
|
|
self._layers[MapRendererLayer.IMAGE].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_FLOOR,
|
|
MAP_DATA_PARAMETER_PIXELS: [
|
|
val
|
|
for sublist in sorted(
|
|
floor_pixels,
|
|
key=cmp_to_key(
|
|
DreameVacuumMapDataRenderer._coordinate_tuple_sort
|
|
),
|
|
)
|
|
for val in sublist
|
|
],
|
|
}
|
|
)
|
|
|
|
if wall_pixels:
|
|
self._layers[MapRendererLayer.IMAGE].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_WALL,
|
|
MAP_DATA_PARAMETER_PIXELS: [
|
|
val
|
|
for sublist in sorted(
|
|
wall_pixels,
|
|
key=cmp_to_key(
|
|
DreameVacuumMapDataRenderer._coordinate_tuple_sort
|
|
),
|
|
)
|
|
for val in sublist
|
|
],
|
|
}
|
|
)
|
|
|
|
if segments:
|
|
for (k, v) in segments.items():
|
|
name = None
|
|
if map_data.segments:
|
|
name = f"Room {k}"
|
|
if k in map_data.segments:
|
|
name = map_data.segments[k].name
|
|
self._layers[MapRendererLayer.IMAGE].append(
|
|
{
|
|
MAP_DATA_PARAMETER_TYPE: MAP_DATA_PARAMETER_SEGMENT,
|
|
MAP_DATA_PARAMETER_PIXELS: [
|
|
val
|
|
for sublist in sorted(
|
|
v,
|
|
key=cmp_to_key(
|
|
DreameVacuumMapDataRenderer._coordinate_tuple_sort
|
|
),
|
|
)
|
|
for val in sublist
|
|
],
|
|
MAP_DATA_PARAMETER_META_DATA: {
|
|
MAP_DATA_PARAMETER_SEGMENT_ID: k,
|
|
MAP_DATA_PARAMETER_ACTIVE: True
|
|
if map_data.active_segments
|
|
and k in map_data.active_segments
|
|
else False,
|
|
MAP_DATA_PARAMETER_NAME: name,
|
|
},
|
|
}
|
|
)
|
|
|
|
for layers in self._layers[MapRendererLayer.IMAGE]:
|
|
pixels = layers[MAP_DATA_PARAMETER_PIXELS]
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS] = {
|
|
MAP_DATA_PARAMETER_X: {
|
|
MAP_DATA_PARAMETER_MIN: 65535,
|
|
MAP_DATA_PARAMETER_MAX: -65535,
|
|
MAP_DATA_PARAMETER_MID: None,
|
|
MAP_DATA_PARAMETER_AVG: None,
|
|
},
|
|
MAP_DATA_PARAMETER_Y: {
|
|
MAP_DATA_PARAMETER_MIN: 65535,
|
|
MAP_DATA_PARAMETER_MAX: -65535,
|
|
MAP_DATA_PARAMETER_MID: None,
|
|
MAP_DATA_PARAMETER_AVG: None,
|
|
},
|
|
MAP_DATA_PARAMETER_PIXEL_COUNT: len(pixels) / 2,
|
|
}
|
|
|
|
sum_x = 0
|
|
sum_y = 0
|
|
for i in range(0, len(pixels), 2):
|
|
sum_x = sum_x + pixels[i]
|
|
sum_y = sum_y + pixels[i + 1]
|
|
|
|
if pixels[i] < layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MIN]:
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MIN] = pixels[i]
|
|
|
|
if pixels[i] > layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MAX]:
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MAX] = pixels[i]
|
|
|
|
if pixels[i + 1] < layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MIN]:
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MIN] = pixels[i + 1]
|
|
|
|
if pixels[i + 1] > layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MAX]:
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MAX] = pixels[i + 1]
|
|
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MID] = round(
|
|
(
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MAX]
|
|
+ layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_MIN]
|
|
)
|
|
/ 2
|
|
)
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MID] = round(
|
|
(
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MAX]
|
|
+ layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_MIN]
|
|
)
|
|
/ 2
|
|
)
|
|
|
|
if sum_x:
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_X][MAP_DATA_PARAMETER_AVG] = round(
|
|
sum_x / (len(pixels) / 2))
|
|
if sum_y:
|
|
layers[MAP_DATA_PARAMETER_DIMENSIONS][MAP_DATA_PARAMETER_Y][MAP_DATA_PARAMETER_AVG] = round(
|
|
sum_y / (len(pixels) / 2))
|
|
|
|
current_x_start = -65535
|
|
current_y = -65535
|
|
current_count = 0
|
|
compressed_pixels = []
|
|
|
|
for i in range(0, len(pixels), 2):
|
|
x = pixels[i]
|
|
y = pixels[i + 1]
|
|
|
|
if y != current_y or x > (current_x_start + current_count):
|
|
compressed_pixels.extend(
|
|
[current_x_start, current_y, current_count]
|
|
)
|
|
current_x_start = x
|
|
current_y = y
|
|
current_count = 1
|
|
elif x != current_x_start:
|
|
current_count = current_count + 1
|
|
|
|
compressed_pixels.extend(
|
|
[current_x_start, current_y, current_count])
|
|
layers[MAP_DATA_PARAMETER_COMPRESSED_PIXELS] = compressed_pixels[3:]
|
|
layers[MAP_DATA_PARAMETER_PIXELS] = []
|
|
|
|
map_data_json[MAP_DATA_PARAMETER_LAYERS].extend(
|
|
self._layers[MapRendererLayer.IMAGE])
|
|
|
|
self._map_data = map_data
|
|
self._map_data_json = map_data_json
|
|
_LOGGER.debug(
|
|
"Render Map Data: %s:%s took: %.2f",
|
|
map_data.map_id,
|
|
map_data.frame_id,
|
|
time.time() - now,
|
|
)
|
|
self.render_complete = True
|
|
return self._to_buffer(
|
|
self._default_map_image,
|
|
json.dumps(self._map_data_json, separators=(",", ":")),
|
|
)
|
|
|
|
@property
|
|
def default_map_image(self) -> bytes:
|
|
return self._to_buffer(self._default_map_image, self._default_map_data)
|
|
|
|
@property
|
|
def disconnected_map_image(self) -> bytes:
|
|
return self.default_map_image
|
|
|
|
|
|
class DreameVacuumMapRenderer:
|
|
def __init__(self, color_scheme: str = None, icon_set: str = None, map_objects: list[str] = None, robot_shape: int = 0) -> None:
|
|
self.color_scheme: MapRendererColorScheme = MAP_COLOR_SCHEME_LIST.get(
|
|
color_scheme, MapRendererColorScheme())
|
|
self.icon_set: int = MAP_ICON_SET_LIST.get(icon_set, 0)
|
|
self.config: MapRendererConfig = MapRendererConfig()
|
|
if map_objects is not None:
|
|
for attr in self.config.__dict__.keys():
|
|
if attr not in map_objects:
|
|
setattr(self.config, attr, False)
|
|
|
|
self._map_data: MapData = None
|
|
self.render_complete: bool = True
|
|
self._layers: dict[MapRendererLayer, Any] = {}
|
|
self._robot_status: int = None
|
|
self._robot_shape: int = robot_shape
|
|
self._calibration_points: dict[str, int] = None
|
|
self._default_calibration_points: dict[str, int] = [
|
|
{MAP_PARAMETER_VACUUM: {MAP_DATA_PARAMETER_X: 0, MAP_DATA_PARAMETER_Y: 0},
|
|
MAP_PARAMETER_MAP: {MAP_DATA_PARAMETER_X: 0, MAP_DATA_PARAMETER_Y: 0}},
|
|
{MAP_PARAMETER_VACUUM: {MAP_DATA_PARAMETER_X: 1000, MAP_DATA_PARAMETER_Y: 0},
|
|
MAP_PARAMETER_MAP: {MAP_DATA_PARAMETER_X: 0, MAP_DATA_PARAMETER_Y: 0}},
|
|
{MAP_PARAMETER_VACUUM: {MAP_DATA_PARAMETER_X: 0, MAP_DATA_PARAMETER_Y: 1000},
|
|
MAP_PARAMETER_MAP: {MAP_DATA_PARAMETER_X: 0, MAP_DATA_PARAMETER_Y: 0}},
|
|
]
|
|
|
|
self._image = None
|
|
self._charger_icon = None
|
|
self._robot_icon = None
|
|
self._robot_charging_icon = None
|
|
self._robot_cleaning_icon = None
|
|
self._robot_warning_icon = None
|
|
self._robot_sleeping_icon = None
|
|
self._robot_washing_icon = None
|
|
self._robot_cleaning_direction_icon = None
|
|
self._obstacle_background = None
|
|
|
|
default_map_image = Image.open(
|
|
BytesIO(base64.b64decode(DEFAULT_MAP_IMAGE))
|
|
).convert("RGBA")
|
|
self._default_map_image = ImageOps.expand(
|
|
default_map_image.resize(
|
|
(
|
|
int(default_map_image.size[0] * 0.8),
|
|
int(default_map_image.size[1] * 0.8),
|
|
)
|
|
),
|
|
border=(50, 75, 50, 75),
|
|
)
|
|
|
|
icon_set = SEGMENT_ICONS_DREAME
|
|
repeats = MAP_ICON_REPEATS_DREAME
|
|
suction_level = MAP_ICON_SUCTION_LEVEL_DREAME
|
|
water_volume = MAP_ICON_WATER_VOLUME_DREAME
|
|
cleaning_mode = MAP_ICON_CLEANING_MODE_DREAME
|
|
|
|
self._segment_icons = {}
|
|
if self.icon_set == 1:
|
|
icon_set = SEGMENT_ICONS_DREAME_OLD
|
|
elif self.icon_set == 2:
|
|
icon_set = SEGMENT_ICONS_MIJIA
|
|
repeats = MAP_ICON_REPEATS_MIJIA
|
|
suction_level = MAP_ICON_SUCTION_LEVEL_MIJIA
|
|
water_volume = MAP_ICON_WATER_VOLUME_MIJIA
|
|
cleaning_mode = MAP_ICON_CLEANING_MODE_MIJIA
|
|
elif self.icon_set == 3:
|
|
icon_set = SEGMENT_ICONS_MATERIAL
|
|
repeats = MAP_ICON_REPEATS_MATERIAL
|
|
suction_level = MAP_ICON_SUCTION_LEVEL_MATERIAL
|
|
water_volume = MAP_ICON_WATER_VOLUME_MATERIAL
|
|
cleaning_mode = MAP_ICON_CLEANING_MODE_MATERIAL
|
|
|
|
self._cleaning_times_icon = [Image.open(BytesIO(base64.b64decode(icon))).convert("RGBA") for icon in repeats]
|
|
self._suction_level_icon = [Image.open(BytesIO(base64.b64decode(icon))).convert("RGBA") for icon in suction_level]
|
|
self._water_volume_icon = [Image.open(BytesIO(base64.b64decode(icon))).convert("RGBA") for icon in water_volume]
|
|
self._cleaning_mode_icon = [Image.open(BytesIO(base64.b64decode(icon))).convert("RGBA") for icon in cleaning_mode]
|
|
|
|
for (k, v) in icon_set.items():
|
|
self._segment_icons[k] = Image.open(BytesIO(base64.b64decode(v))).convert(
|
|
"RGBA"
|
|
)
|
|
if self.color_scheme.invert:
|
|
enhancer = ImageEnhance.Brightness(self._segment_icons[k])
|
|
self._segment_icons[k] = enhancer.enhance(0.1)
|
|
|
|
self._obstacle_icons = {}
|
|
for (k, v) in OBSTACLE_TYPE_TO_ICON.items():
|
|
self._obstacle_icons[k] = Image.open(BytesIO(base64.b64decode(v))).convert(
|
|
"RGBA"
|
|
)
|
|
|
|
self.font_file = zlib.decompress(
|
|
base64.b64decode(MAP_FONT), zlib.MAX_WBITS | 32)
|
|
|
|
@staticmethod
|
|
def _to_buffer(image) -> bytes:
|
|
if image:
|
|
buffer = io.BytesIO()
|
|
image.save(buffer, format="PNG")
|
|
return buffer.getvalue()
|
|
|
|
@staticmethod
|
|
def _set_icon_color(image, size, color):
|
|
ico = image.resize((int(size), int(size)))
|
|
pixdata = ico.load()
|
|
for yy in range(ico.size[1]):
|
|
for xx in range(ico.size[0]):
|
|
if (
|
|
pixdata[xx, yy][0] > 80
|
|
and pixdata[xx, yy][1] > 80
|
|
and pixdata[xx, yy][2] > 80
|
|
and pixdata[xx, yy][3] > 80
|
|
):
|
|
pixdata[xx, yy] = color
|
|
|
|
return ico
|
|
|
|
@staticmethod
|
|
def _calculate_bounds(dimensions, segments) -> list[int]:
|
|
if segments:
|
|
min_x = dimensions.width - 1
|
|
min_y = dimensions.height - 1
|
|
max_x = 0
|
|
max_y = 0
|
|
for segment in segments.values():
|
|
p = segment.to_coord(dimensions)
|
|
x_coords = [int(p.x0), int(p.x1)]
|
|
y_coords = [int(p.y0), int(p.y1)]
|
|
min_x = min(min(x_coords), min_x)
|
|
max_x = max(max(x_coords), max_x)
|
|
min_y = min(min(y_coords), min_y)
|
|
max_y = max(max(y_coords), max_y)
|
|
|
|
return [min_x, min_y, max_x, max_y]
|
|
|
|
@staticmethod
|
|
def _calculate_padding(dimensions, active_areas, no_mopping_areas, no_go_areas, walls, segments, padding, min_width, min_height, scale) -> list[int]:
|
|
min_x = 0
|
|
min_y = 0
|
|
max_x = dimensions.width
|
|
max_y = dimensions.height
|
|
|
|
if segments:
|
|
for segment in segments.values():
|
|
p = segment.to_coord(dimensions)
|
|
x_coords = sorted([int(p.x0), int(p.x1)])
|
|
y_coords = sorted([int(p.y0), int(p.y1)])
|
|
min_x = min(x_coords[0], min_x)
|
|
max_x = max(x_coords[1], max_x)
|
|
min_y = min(y_coords[0], min_y)
|
|
max_y = max(y_coords[1], max_y)
|
|
|
|
if active_areas:
|
|
for area in active_areas:
|
|
p = area.to_coord(dimensions)
|
|
x_coords = [p.x0, p.x1, p.x2, p.x3]
|
|
y_coords = [p.y0, p.y1, p.y2, p.y3]
|
|
min_x = min(min(x_coords), min_x)
|
|
max_x = max(max(x_coords), max_x)
|
|
min_y = min(min(y_coords), min_y)
|
|
max_y = max(max(y_coords), max_y)
|
|
|
|
|
|
if no_mopping_areas:
|
|
for area in no_mopping_areas:
|
|
p = area.to_coord(dimensions)
|
|
x_coords = [p.x0, p.x1, p.x2, p.x3]
|
|
y_coords = [p.y0, p.y1, p.y2, p.y3]
|
|
min_x = min(min(x_coords), min_x)
|
|
max_x = max(max(x_coords), max_x)
|
|
min_y = min(min(y_coords), min_y)
|
|
max_y = max(max(y_coords), max_y)
|
|
|
|
if no_go_areas:
|
|
for area in no_go_areas:
|
|
p = area.to_coord(dimensions)
|
|
x_coords = [p.x0, p.x1, p.x2, p.x3]
|
|
y_coords = [p.y0, p.y1, p.y2, p.y3]
|
|
min_x = min(min(x_coords), min_x)
|
|
max_x = max(max(x_coords), max_x)
|
|
min_y = min(min(y_coords), min_y)
|
|
max_y = max(max(y_coords), max_y)
|
|
|
|
if walls:
|
|
for wall in walls:
|
|
p = wall.to_coord(dimensions)
|
|
x_coords = [p.x0, p.x1]
|
|
y_coords = [p.y0, p.y1]
|
|
min_x = min(min(x_coords), min_x)
|
|
max_x = max(max(x_coords), max_x)
|
|
min_y = min(min(y_coords), min_y)
|
|
max_y = max(max(y_coords), max_y)
|
|
|
|
if min_x < 0:
|
|
padding[0] = padding[0] + int(-min_x)
|
|
if max_x > dimensions.width:
|
|
padding[2] = padding[2] + int(max_x - dimensions.width)
|
|
if min_y < 0:
|
|
padding[1] = padding[1] + int(-min_y)
|
|
if max_y > dimensions.height:
|
|
padding[3] = padding[3] + int(max_y - dimensions.height)
|
|
|
|
if dimensions.width + padding[0] + padding[2] < min_width:
|
|
size = int((min_width - dimensions.width + padding[0] + padding[2]) / 2)
|
|
padding[0] = padding[0] + size
|
|
padding[2] = padding[2] + size
|
|
|
|
if dimensions.height + padding[1] + padding[3] < min_height:
|
|
size = int((min_height - dimensions.height + padding[1] + padding[3]) / 2)
|
|
padding[1] = padding[1] + size
|
|
padding[3] = padding[3] + size
|
|
|
|
for k in range(4):
|
|
padding[k] = padding[k] * scale
|
|
|
|
return padding
|
|
|
|
@staticmethod
|
|
def _calculate_calibration_points(map_data: MapData) -> dict[str, int] | None:
|
|
if (map_data.dimensions.width * map_data.dimensions.height) > 0:
|
|
calibration_points = []
|
|
for point in [Point(0, 0), Point(1000, 0), Point(0, 1000)]:
|
|
img_point = point.to_img(map_data.dimensions).rotated(
|
|
map_data.dimensions, map_data.rotation
|
|
)
|
|
calibration_points.append(
|
|
{
|
|
MAP_PARAMETER_VACUUM: {MAP_DATA_PARAMETER_X: point.x, MAP_DATA_PARAMETER_Y: point.y},
|
|
MAP_PARAMETER_MAP: {MAP_DATA_PARAMETER_X: int(img_point.x), MAP_DATA_PARAMETER_Y: int(img_point.y)},
|
|
}
|
|
)
|
|
return calibration_points
|
|
|
|
def render_map(self, map_data: MapData, robot_status: int = 0) -> bytes:
|
|
if map_data is None or map_data.empty_map or (map_data.dimensions.width * map_data.dimensions.height) < 2:
|
|
return self.default_map_image
|
|
|
|
self.render_complete = False
|
|
now = time.time()
|
|
|
|
if map_data.saved_map:
|
|
robot_status = 0
|
|
try:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.dimensions != map_data.dimensions
|
|
or self._map_data.map_id != map_data.map_id
|
|
or self._map_data.saved_map_status != map_data.saved_map_status
|
|
):
|
|
self._map_data = None
|
|
|
|
if (
|
|
self._map_data
|
|
and self._map_data == map_data
|
|
and self._robot_status == robot_status
|
|
and self._map_data.segments == map_data.segments
|
|
and self._map_data.frame_id == map_data.frame_id
|
|
and self._image
|
|
):
|
|
self.render_complete = True
|
|
_LOGGER.info("Skip render frame, map data not changed")
|
|
return self._to_buffer(self._image)
|
|
|
|
scale = 4 if map_data.saved_map_status == 2 or map_data.saved_map else 3
|
|
|
|
if not map_data.saved_map:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.segments != map_data.segments
|
|
or self._map_data.dimensions != map_data.dimensions
|
|
):
|
|
map_data.dimensions.bounds = DreameVacuumMapRenderer._calculate_bounds(
|
|
map_data.dimensions,
|
|
map_data.segments
|
|
)
|
|
|
|
if self._map_data and self._map_data.dimensions.bounds != map_data.dimensions.bounds:
|
|
self._map_data = None
|
|
else:
|
|
map_data.dimensions.bounds = self._map_data.dimensions.bounds
|
|
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.active_areas != map_data.active_areas
|
|
or self._map_data.no_mopping_areas != map_data.no_mopping_areas
|
|
or self._map_data.no_go_areas != map_data.no_go_areas
|
|
or self._map_data.walls != map_data.walls
|
|
or self._map_data.segments != map_data.segments
|
|
or self._map_data.dimensions != map_data.dimensions
|
|
or self._map_data.restored_map != map_data.restored_map
|
|
):
|
|
map_data.dimensions.padding = DreameVacuumMapRenderer._calculate_padding(
|
|
map_data.dimensions,
|
|
map_data.active_areas,
|
|
map_data.no_mopping_areas,
|
|
map_data.no_go_areas,
|
|
map_data.walls,
|
|
map_data.segments,
|
|
[14, 14, 14, 14],
|
|
120,
|
|
80,
|
|
scale
|
|
)
|
|
|
|
if self._map_data and self._map_data.dimensions.padding != map_data.dimensions.padding:
|
|
self._map_data = None
|
|
else:
|
|
map_data.dimensions.padding = self._map_data.dimensions.padding
|
|
|
|
map_data.dimensions.scale = scale
|
|
|
|
if self._map_data and self._map_data.dimensions.scale != scale:
|
|
self._map_data = None
|
|
|
|
if self._map_data is None or self._map_data.rotation != map_data.rotation:
|
|
self._charger_icon = None
|
|
self._robot_sleeping_icon = None
|
|
self._obstacle_background = None
|
|
|
|
if (
|
|
self._map_data is None
|
|
):
|
|
self._robot_icon = None
|
|
self._robot_charging_icon = None
|
|
self._robot_cleaning_icon = None
|
|
self._robot_warning_icon = None
|
|
self._robot_washing_icon = None
|
|
self._robot_cleaning_direction_icon = None
|
|
|
|
if (
|
|
self._map_data is None
|
|
or not self._layers.get(MapRendererLayer.IMAGE)
|
|
or self._map_data.active_segments != map_data.active_segments
|
|
or self._map_data.active_areas != map_data.active_areas
|
|
or self._map_data.segments != map_data.segments
|
|
or self._map_data.data != map_data.data
|
|
):
|
|
area_colors = {}
|
|
# as implemented on the app
|
|
area_colors[MapPixelType.OUTSIDE.value] = self.color_scheme.outside
|
|
area_colors[MapPixelType.WALL.value] = self.color_scheme.wall
|
|
area_colors[MapPixelType.FLOOR.value] = self.color_scheme.floor
|
|
area_colors[MapPixelType.NEW_SEGMENT.value] = self.color_scheme.new_segment
|
|
area_colors[MapPixelType.UNKNOWN.value] = self.color_scheme.floor
|
|
area_colors[MapPixelType.OBSTACLE_WALL.value] = self.color_scheme.wall
|
|
area_colors[MapPixelType.NEW_SEGMENT_UNKNOWN.value] = self.color_scheme.new_segment
|
|
|
|
if map_data.segments is not None:
|
|
for (k, v) in map_data.segments.items():
|
|
if self.config.color:
|
|
if map_data.active_segments and k not in map_data.active_segments:
|
|
area_colors[k] = self.color_scheme.passive_segment
|
|
elif v.color_index is not None:
|
|
area_colors[k] = self.color_scheme.segment[
|
|
v.color_index
|
|
][0]
|
|
else:
|
|
area_colors[k] = area_colors[MapPixelType.FLOOR.value]
|
|
|
|
pixels = np.full(
|
|
(
|
|
map_data.dimensions.height,
|
|
map_data.dimensions.width,
|
|
4,
|
|
),
|
|
area_colors[MapPixelType.OUTSIDE.value],
|
|
dtype=np.uint8,
|
|
)
|
|
|
|
min_x = map_data.dimensions.width - 1
|
|
min_y = map_data.dimensions.height - 1
|
|
max_x = 0
|
|
max_y = 0
|
|
for y in range(map_data.dimensions.height):
|
|
for x in range(map_data.dimensions.width):
|
|
px_type = int(
|
|
map_data.pixel_type[x, map_data.dimensions.height - y - 1])
|
|
if px_type != MapPixelType.OUTSIDE.value:
|
|
pixels[y, x] = area_colors[px_type] if px_type in area_colors else area_colors[MapPixelType.NEW_SEGMENT.value]
|
|
|
|
max_x = max(x, max_x)
|
|
min_x = min(x, min_x)
|
|
max_y = max(y, max_y)
|
|
min_y = min(y, min_y)
|
|
|
|
if map_data.dimensions.bounds:
|
|
#min_x = max(0, min(map_data.dimensions.bounds[0], min_x))
|
|
#max_x = min((map_data.dimensions.width - 1), max(map_data.dimensions.bounds[2], max_x))
|
|
#min_y = max(0, min(map_data.dimensions.bounds[1], min_y))
|
|
#max_y = min((map_data.dimensions.height - 1), max(map_data.dimensions.bounds[3], max_y))
|
|
min_x = max(min(map_data.dimensions.bounds[0], min_x), min_x)
|
|
max_x = min(max(map_data.dimensions.bounds[2], max_x), max_x)
|
|
min_y = max(min(map_data.dimensions.bounds[1], min_y), min_y)
|
|
max_y = min(max(map_data.dimensions.bounds[3], max_y), max_y)
|
|
|
|
if (
|
|
(
|
|
min_x != (map_data.dimensions.width - 1) and
|
|
min_y != (map_data.dimensions.height - 1) and
|
|
max_x != 0 and
|
|
max_y != 0
|
|
) and
|
|
(
|
|
min_x != 0 or
|
|
min_y != 0 or
|
|
max_x != (map_data.dimensions.width - 1) or
|
|
max_y != (map_data.dimensions.height - 1)
|
|
)
|
|
):
|
|
map_data.dimensions.crop = [min_x * scale, min_y * scale, (map_data.dimensions.width - (
|
|
max_x + 1)) * scale, (map_data.dimensions.height - (max_y + 1)) * scale]
|
|
pixels = pixels[min_y:(max_y + 1), min_x:(max_x + 1)]
|
|
|
|
if self._map_data and self._map_data.dimensions.crop != map_data.dimensions.crop:
|
|
self._map_data = None
|
|
|
|
self._layers[MapRendererLayer.IMAGE] = ImageOps.expand(
|
|
Image.fromarray(pixels.repeat(
|
|
scale, axis=0).repeat(scale, axis=1)),
|
|
border=tuple(map_data.dimensions.padding)
|
|
)
|
|
else:
|
|
map_data.dimensions.crop = self._map_data.dimensions.crop
|
|
|
|
self._calibration_points = self._calculate_calibration_points(
|
|
map_data)
|
|
|
|
image = self.render_objects(
|
|
map_data,
|
|
robot_status,
|
|
self._layers[MapRendererLayer.IMAGE],
|
|
2,
|
|
)
|
|
|
|
if map_data.rotation == 90:
|
|
image = image.transpose(Image.ROTATE_90)
|
|
elif map_data.rotation == 180:
|
|
image = image.transpose(Image.ROTATE_180)
|
|
elif map_data.rotation == 270:
|
|
image = image.transpose(Image.ROTATE_270)
|
|
|
|
_LOGGER.info(
|
|
"Render frame: %s:%s took: %.2f",
|
|
map_data.map_id,
|
|
map_data.frame_id,
|
|
time.time() - now
|
|
)
|
|
|
|
self._map_data = map_data
|
|
self._robot_status = robot_status
|
|
self._image = image
|
|
except Exception:
|
|
_LOGGER.error("Map render Failed: %s", traceback.format_exc())
|
|
|
|
self.render_complete = True
|
|
return self._to_buffer(self._image)
|
|
|
|
def render_objects(
|
|
self,
|
|
map_data,
|
|
robot_status,
|
|
map_image,
|
|
scale,
|
|
):
|
|
if self._map_data is None or not self._layers.get(MapRendererLayer.OBJECTS):
|
|
self._layers[MapRendererLayer.OBJECTS] = Image.new(
|
|
"RGBA",
|
|
[int(map_image.size[0] * scale),
|
|
int(map_image.size[1] * scale)],
|
|
(255, 255, 255, 0),
|
|
)
|
|
layer = self._layers[MapRendererLayer.OBJECTS]
|
|
layer.paste((255, 255, 255, 0), [
|
|
0, 0, layer.size[0], layer.size[1]])
|
|
|
|
line_width = 3
|
|
border_width = 2
|
|
|
|
if map_data.rotation == 0 or map_data.rotation == 180:
|
|
width = (map_data.dimensions.width) + ((map_data.dimensions.padding[0] + map_data.dimensions.padding[2] - map_data.dimensions.crop[0] - map_data.dimensions.crop[2]) / map_data.dimensions.scale)
|
|
robot_icon_size = width * 0.037
|
|
icon_size = width * 0.03
|
|
else:
|
|
height = (map_data.dimensions.height) + ((map_data.dimensions.padding[1] + map_data.dimensions.padding[3] - map_data.dimensions.crop[1] - map_data.dimensions.crop[3]) / map_data.dimensions.scale)
|
|
robot_icon_size = height * 0.037
|
|
icon_size = height * 0.03
|
|
|
|
robot_icon_size = max(7, min(14, robot_icon_size))
|
|
icon_size = max(5, min(10, icon_size))
|
|
|
|
if map_data.path and self.config.path:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.path != map_data.path
|
|
or not self._layers.get(MapRendererLayer.PATH)
|
|
):
|
|
self._layers[MapRendererLayer.PATH] = self.render_path(
|
|
map_data.path,
|
|
self.color_scheme.path,
|
|
layer,
|
|
map_data.dimensions,
|
|
line_width,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.PATH])
|
|
|
|
if map_data.no_mopping_areas and self.config.no_mop:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.no_mopping_areas != map_data.no_mopping_areas
|
|
or not self._layers.get(MapRendererLayer.NO_MOP)
|
|
):
|
|
self._layers[MapRendererLayer.NO_MOP] = self.render_areas(
|
|
map_data.no_mopping_areas,
|
|
self.color_scheme.no_mop_outline,
|
|
self.color_scheme.no_mop,
|
|
layer,
|
|
map_data.dimensions,
|
|
border_width,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.NO_MOP])
|
|
|
|
if map_data.no_go_areas and self.config.no_go:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.no_go_areas != map_data.no_go_areas
|
|
or not self._layers.get(MapRendererLayer.NO_GO)
|
|
):
|
|
self._layers[MapRendererLayer.NO_GO] = self.render_areas(
|
|
map_data.no_go_areas,
|
|
self.color_scheme.no_go_outline,
|
|
self.color_scheme.no_go,
|
|
layer,
|
|
map_data.dimensions,
|
|
border_width,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.NO_GO])
|
|
|
|
if map_data.walls and self.config.virtual_wall:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.walls != map_data.walls
|
|
or not self._layers.get(MapRendererLayer.WALL)
|
|
):
|
|
self._layers[MapRendererLayer.WALL] = self.render_walls(
|
|
map_data.walls,
|
|
self.color_scheme.virtual_wall,
|
|
layer,
|
|
map_data.dimensions,
|
|
line_width,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.WALL])
|
|
|
|
if map_data.active_areas and self.config.active_area:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.active_areas != map_data.active_areas
|
|
or not self._layers.get(MapRendererLayer.ACTIVE_AREA)
|
|
):
|
|
self._layers[MapRendererLayer.ACTIVE_AREA] = self.render_areas(
|
|
map_data.active_areas,
|
|
self.color_scheme.active_area_outline,
|
|
self.color_scheme.active_area,
|
|
layer,
|
|
map_data.dimensions,
|
|
border_width,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.ACTIVE_AREA])
|
|
|
|
if map_data.active_points and self.config.active_point:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.active_points != map_data.active_points
|
|
or not self._layers.get(MapRendererLayer.ACTIVE_POINT)
|
|
):
|
|
self._layers[MapRendererLayer.ACTIVE_POINT] = self.render_points(
|
|
map_data.active_points,
|
|
self.color_scheme.active_point_outline,
|
|
self.color_scheme.active_point,
|
|
layer,
|
|
map_data.dimensions,
|
|
border_width,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.ACTIVE_POINT])
|
|
|
|
if map_data.segments and (self.config.icon or self.config.name or self.config.order or self.config.suction_level or self.config.water_volume or self.config.cleaning_times or self.config.cleaning_mode):
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.segments != map_data.segments
|
|
or self._map_data.rotation != map_data.rotation
|
|
or bool(self._map_data.cleanset) != bool(map_data.cleanset)
|
|
or not self._layers.get(MapRendererLayer.SEGMENTS)
|
|
):
|
|
if MapRendererLayer.SEGMENTS not in self._layers:
|
|
self._layers[MapRendererLayer.SEGMENTS] = {}
|
|
else:
|
|
for k in list(self._layers[MapRendererLayer.SEGMENTS].keys()).copy():
|
|
if k not in map_data.segments:
|
|
del self._layers[MapRendererLayer.SEGMENTS][k]
|
|
|
|
for k, v in map_data.segments.items():
|
|
if (
|
|
self._map_data is None
|
|
or k not in self._layers[MapRendererLayer.SEGMENTS]
|
|
or not self._map_data.segments
|
|
or k not in self._map_data.segments
|
|
or self._map_data.segments[k] != v
|
|
or self._map_data.rotation != map_data.rotation
|
|
or bool(self._map_data.cleanset) != bool(map_data.cleanset)
|
|
):
|
|
self._layers[MapRendererLayer.SEGMENTS][k] = self.render_segment(
|
|
v,
|
|
bool(map_data.cleanset),
|
|
layer,
|
|
map_data.dimensions,
|
|
int(icon_size * map_data.dimensions.scale),
|
|
map_data.rotation,
|
|
scale,
|
|
)
|
|
|
|
if self._layers[MapRendererLayer.SEGMENTS]:
|
|
for k, v in sorted(self._layers[MapRendererLayer.SEGMENTS].items(), reverse=True):
|
|
layer = Image.alpha_composite(layer, v)
|
|
|
|
if map_data.charger_position and self.config.charger:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.charger_position != map_data.charger_position
|
|
or self._map_data.rotation != map_data.rotation
|
|
or bool(self._robot_status > 5) != bool(robot_status > 5)
|
|
or not self._layers.get(MapRendererLayer.CHARGER)
|
|
):
|
|
#def correct_charger_position(chargerPos, pixel_type, width, height, x, y, gridWidth, borderValue):
|
|
# newChargerPos = copy.deepcopy(chargerPos)
|
|
# tmpAngle = newChargerPos.a % 360
|
|
|
|
# if tmpAngle < 0:
|
|
# tmpAngle += 360
|
|
|
|
# chargerX = int((newChargerPos.x - x) / gridWidth)
|
|
# chargerY = int((newChargerPos.y - y) / gridWidth)
|
|
# value = pixel_type[chargerX, chargerY]
|
|
|
|
# if value == borderValue or chargerX < 0 or chargerX >= width or chargerY < 0 or chargerY >= height:
|
|
# return chargerPos
|
|
|
|
# isChargerInMap = value != 0
|
|
# delta = 3
|
|
|
|
# for crossDelta in range(4):
|
|
# if tmpAngle > 45 and tmpAngle < 135 or tmpAngle > 225 and tmpAngle < 315:
|
|
# startY = 0 if ((chargerY - delta) < 0) else (chargerY - delta)
|
|
# endY = (height - 1) if ((chargerY + delta) > (height - 1)) else (chargerY + delta)
|
|
|
|
# if tmpAngle > 45 and tmpAngle < 135:
|
|
# if isChargerInMap:
|
|
# endY = chargerY
|
|
# else:
|
|
# startY = chargerY
|
|
# else:
|
|
# if isChargerInMap:
|
|
# startY = chargerY
|
|
# else:
|
|
# endY = chargerY
|
|
|
|
# findY = -1
|
|
|
|
# for j in range(startY, endY + 1):
|
|
# startX = -1
|
|
|
|
# for i in range(width):
|
|
# leftIndex = (i - 1) if ((i - 1) >= 0) else -1
|
|
# rightIndex = (i + 1) if ((i + 1) < width) else -1
|
|
|
|
# if pixel_type[i, j] == borderValue and (i == 0 or leftIndex != -1 and pixel_type[leftIndex, j] != borderValue):
|
|
# startX = i
|
|
|
|
# if pixel_type[i + 1, j] != borderValue:
|
|
# if (chargerX + crossDelta) >= startX and (chargerX - crossDelta) <= i:
|
|
# if findY == -1:
|
|
# findY = j
|
|
# elif abs(chargerY - j) < abs(findY - j):
|
|
# findY = j
|
|
# startX = -1
|
|
|
|
# continue
|
|
|
|
# if pixel_type[i, j] == borderValue and startX != -1 and (i == (width - 1) or rightIndex != -1 and pixel_type[rightIndex, j] != borderValue):
|
|
# if (chargerX + crossDelta) >= startX and (chargerX - crossDelta) <= i:
|
|
# if findY == -1:
|
|
# findY = j
|
|
# elif abs(chargerY - j) < abs(findY - j):
|
|
# findY = j
|
|
|
|
# startX = -1
|
|
# if findY != -1:
|
|
# newChargerPos.y = y + findY * gridWidth
|
|
# break
|
|
# else:
|
|
# _startX = 0 if ((chargerX - delta) < 0) else (chargerX - delta)
|
|
# endX = (width - 1) if ((chargerX + delta) > (width - 1)) else (chargerX + delta)
|
|
|
|
# if tmpAngle >= 0 and tmpAngle <= 45 or tmpAngle >= 315 and tmpAngle < 360:
|
|
# if isChargerInMap:
|
|
# endX = chargerX
|
|
# else:
|
|
# _startX = chargerX
|
|
# else:
|
|
# if isChargerInMap:
|
|
# _startX = chargerX
|
|
# else:
|
|
# endX = chargerX
|
|
|
|
# findX = -1
|
|
|
|
# for _i in range(_startX, endX + 1):
|
|
# _startY = -1
|
|
|
|
# for _j in range(height):
|
|
# topIndex = (_j - 1) if ((_j - 1) >= 0) else -1
|
|
# bottomIndex = (_j + 1) if ((_j + 1) < height) else -1
|
|
|
|
# if pixel_type[_i, _j] == borderValue and (_j == 0 or topIndex != -1 and pixel_type[_i, topIndex] != borderValue):
|
|
# _startY = _j
|
|
|
|
# if pixel_type[_i, (_j + 1)] != borderValue:
|
|
# if ((chargerY + crossDelta) >= _startY) and ((chargerY - crossDelta) <= _j):
|
|
# if findX == -1:
|
|
# findX = _i
|
|
# elif abs(chargerX - _i) < abs(findX - _i):
|
|
# findX = _i
|
|
# _startY = -1
|
|
|
|
# continue
|
|
|
|
# if pixel_type[_i, _j] == borderValue and _startY != -1 and (_j == height - 1 or bottomIndex != -1 and pixel_type[_i, bottomIndex] != borderValue):
|
|
# if ((chargerY + crossDelta) >= _startY) and ((chargerY - crossDelta) <= _j):
|
|
# if findX == -1:
|
|
# findX = _i
|
|
# elif abs(chargerX - _i) < abs(findX - _i):
|
|
# findX = _i
|
|
|
|
# _startY = -1
|
|
|
|
# if findX != -1:
|
|
# newChargerPos.x = x + findX * gridWidth
|
|
# break
|
|
|
|
# return newChargerPos
|
|
|
|
charger_position = map_data.charger_position
|
|
if self._robot_shape != 1 and self.icon_set == 2:
|
|
offset = int(robot_icon_size * 21.42)
|
|
charger_position = Point(
|
|
charger_position.x - offset * math.cos(charger_position.a * math.pi / 180),
|
|
charger_position.y - offset * math.sin(charger_position.a * math.pi / 180),
|
|
charger_position.a
|
|
)
|
|
|
|
self._layers[MapRendererLayer.CHARGER] = self.render_charger(
|
|
charger_position,
|
|
robot_status,
|
|
layer,
|
|
map_data.dimensions,
|
|
int((robot_icon_size * map_data.dimensions.scale) * 1.2),
|
|
map_data.rotation,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.CHARGER])
|
|
|
|
if map_data.robot_position and self.config.robot:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.robot_position != map_data.robot_position
|
|
or self._map_data.charger_position != map_data.charger_position
|
|
or self._map_data.rotation != map_data.rotation
|
|
or self._robot_status != robot_status
|
|
or self._map_data.docked != map_data.docked
|
|
or not self._layers.get(MapRendererLayer.ROBOT)
|
|
):
|
|
robot_position = map_data.robot_position
|
|
|
|
if map_data.docked:
|
|
# Calculate charger angle
|
|
charger_angle = map_data.charger_position.a
|
|
if self._robot_shape != 1:
|
|
offset = int(robot_icon_size * 21.42)
|
|
|
|
if self.icon_set != 2:
|
|
if (
|
|
charger_angle > -45
|
|
and charger_angle < 45
|
|
):
|
|
charger_angle = 0
|
|
elif (
|
|
charger_angle > -45
|
|
and charger_angle <= 45
|
|
or charger_angle > 315
|
|
and charger_angle <= 405
|
|
):
|
|
charger_angle = 0
|
|
elif (
|
|
charger_angle > 45
|
|
and charger_angle <= 135
|
|
or charger_angle > -315
|
|
and charger_angle <= -225
|
|
):
|
|
charger_angle = 90
|
|
elif (
|
|
charger_angle > 135
|
|
and charger_angle <= 225
|
|
or charger_angle > -225
|
|
and charger_angle <= -135
|
|
):
|
|
charger_angle = 180
|
|
elif (
|
|
charger_angle > 225
|
|
and charger_angle <= 315
|
|
or charger_angle > -135
|
|
and charger_angle <= -45
|
|
):
|
|
charger_angle = 270
|
|
else:
|
|
offset = int(robot_icon_size * 35.71)
|
|
|
|
robot_position = Point(
|
|
map_data.charger_position.x + offset * math.cos(charger_angle * math.pi / 180),
|
|
map_data.charger_position.y + offset * math.sin(charger_angle * math.pi / 180),
|
|
charger_angle + 180 if self._robot_shape != 2 else charger_angle
|
|
)
|
|
|
|
self._layers[MapRendererLayer.ROBOT] = self.render_vacuum(
|
|
robot_position,
|
|
robot_status,
|
|
layer,
|
|
map_data.dimensions,
|
|
int(robot_icon_size * map_data.dimensions.scale),
|
|
map_data.rotation,
|
|
scale,
|
|
)
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.ROBOT])
|
|
|
|
if map_data.obstacles and self.config.obstacle:
|
|
if (
|
|
self._map_data is None
|
|
or self._map_data.obstacles != map_data.obstacles
|
|
or self._map_data.rotation != map_data.rotation
|
|
or not self._layers.get(MapRendererLayer.OBSTACLES)
|
|
):
|
|
self._layers[MapRendererLayer.OBSTACLES] = self.render_obstacles(
|
|
map_data.obstacles,
|
|
layer,
|
|
map_data.dimensions,
|
|
int((icon_size * 2) * map_data.dimensions.scale),
|
|
map_data.rotation,
|
|
scale,
|
|
)
|
|
|
|
layer = Image.alpha_composite(
|
|
layer, self._layers[MapRendererLayer.OBSTACLES])
|
|
|
|
if layer.size != map_image.size:
|
|
layer.thumbnail(
|
|
map_image.size, Image.Resampling.BOX, reducing_gap=1.5)
|
|
|
|
return Image.alpha_composite(
|
|
map_image,
|
|
layer,
|
|
)
|
|
|
|
def render_areas(self, areas, color, fill, layer, dimensions, width, scale):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
draw = ImageDraw.Draw(new_layer, "RGBA")
|
|
for area in areas:
|
|
p = area.to_img(dimensions)
|
|
coords = [
|
|
p.x0 * scale,
|
|
p.y0 * scale,
|
|
p.x1 * scale,
|
|
p.y1 * scale,
|
|
p.x2 * scale,
|
|
p.y2 * scale,
|
|
p.x3 * scale,
|
|
p.y3 * scale,
|
|
]
|
|
draw.polygon(coords, fill, color, width=(width * scale))
|
|
return new_layer
|
|
|
|
def render_points(self, points, color, fill, layer, dimensions, width, scale):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
draw = ImageDraw.Draw(new_layer, "RGBA")
|
|
size = 15 * dimensions.grid_size
|
|
for point in points:
|
|
area = Area(point.x - size, point.y - size, point.x + size, point.y -
|
|
size, point.x + size, point.y + size, point.x - size, point.y + size)
|
|
|
|
p = area.to_img(dimensions)
|
|
coords = [
|
|
p.x0 * scale,
|
|
p.y0 * scale,
|
|
p.x1 * scale,
|
|
p.y1 * scale,
|
|
p.x2 * scale,
|
|
p.y2 * scale,
|
|
p.x3 * scale,
|
|
p.y3 * scale,
|
|
]
|
|
draw.polygon(coords, fill, color, width=(width * scale))
|
|
return new_layer
|
|
|
|
def render_walls(self, walls, color, layer, dimensions, width, scale):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
draw = ImageDraw.Draw(new_layer, "RGBA")
|
|
for wall in walls:
|
|
p = wall.to_img(dimensions)
|
|
draw.line(
|
|
[p.x0 * scale, p.y0 * scale, p.x1 * scale, p.y1 * scale],
|
|
color,
|
|
width=(width * scale),
|
|
)
|
|
return new_layer
|
|
|
|
def render_path(self, path, color, layer, dimensions, width, scale):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
draw = ImageDraw.Draw(new_layer, "RGBA")
|
|
sweep = []
|
|
mop = []
|
|
sweep_path = []
|
|
mop_path = []
|
|
path_type = ""
|
|
|
|
for point in path:
|
|
p = point.to_img(dimensions)
|
|
if point.path_type == PathType.LINE:
|
|
l = [p.x * scale, p.y * scale]
|
|
if path_type == PathType.SWEEP_AND_MOP or path_type == PathType.SWEEP:
|
|
sweep_path.extend(l)
|
|
|
|
if path_type == PathType.SWEEP_AND_MOP or path_type == PathType.MOP:
|
|
mop_path.extend(l)
|
|
else:
|
|
if mop_path:
|
|
mop.append(mop_path)
|
|
|
|
if sweep_path:
|
|
sweep.append(sweep_path)
|
|
|
|
path_type = point.path_type
|
|
if path_type == PathType.SWEEP_AND_MOP or path_type == PathType.SWEEP:
|
|
sweep_path = [p.x * scale, p.y * scale]
|
|
else:
|
|
sweep_path = []
|
|
|
|
if path_type == PathType.SWEEP_AND_MOP or path_type == PathType.MOP:
|
|
mop_path = [p.x * scale, p.y * scale]
|
|
else:
|
|
mop_path = []
|
|
|
|
if sweep_path:
|
|
sweep.append(sweep_path)
|
|
|
|
if mop_path:
|
|
mop.append(mop_path)
|
|
|
|
for path in mop:
|
|
size = width * scale * 12
|
|
draw.line(
|
|
path,
|
|
width=int(size),
|
|
fill=(color[0], color[1], color[2], 100),
|
|
joint='curve',
|
|
)
|
|
|
|
for path in sweep:
|
|
size = width * scale
|
|
draw.line(
|
|
path,
|
|
width=int(size),
|
|
fill=color,
|
|
joint='curve',
|
|
)
|
|
size = int(math.floor(size / 2))
|
|
draw.ellipse([
|
|
path[-2] - size,
|
|
path[-1] - size,
|
|
path[-2] + size,
|
|
path[-1] + size,
|
|
],
|
|
fill=color,
|
|
)
|
|
draw.ellipse([
|
|
path[0] - size,
|
|
path[1] - size,
|
|
path[0] + size,
|
|
path[1] + size,
|
|
],
|
|
fill=color,
|
|
)
|
|
|
|
return new_layer
|
|
|
|
def render_charger(
|
|
self, charger_position, robot_status, layer, dimensions, size, map_rotation, scale
|
|
):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
icon_size = int(size * scale)
|
|
if self._charger_icon is None:
|
|
if self.icon_set == 3:
|
|
charger_image = MAP_CHARGER_IMAGE_MATERIAL
|
|
icon_size = int(icon_size * 1.2)
|
|
elif self.icon_set == 2:
|
|
charger_image = MAP_CHARGER_IMAGE_MIJIA
|
|
icon_size = int(icon_size * 1.5)
|
|
else:
|
|
if self._robot_shape == 1:
|
|
charger_image = MAP_CHARGER_VSLAM_IMAGE_DREAME
|
|
icon_size = int(icon_size * 1.5)
|
|
else:
|
|
charger_image = MAP_CHARGER_IMAGE_DREAME
|
|
|
|
self._charger_icon = (
|
|
Image.open(BytesIO(base64.b64decode(charger_image)))
|
|
.convert("RGBA")
|
|
.resize((icon_size, icon_size), resample=Image.Resampling.NEAREST)
|
|
)
|
|
|
|
if self.icon_set == 3:
|
|
self._charger_icon = DreameVacuumMapRenderer._set_icon_color(
|
|
self._charger_icon,
|
|
icon_size,
|
|
(0, 255, 126),
|
|
)
|
|
|
|
if self.color_scheme.dark:
|
|
enhancer = ImageEnhance.Brightness(self._charger_icon)
|
|
self._charger_icon = enhancer.enhance(0.7)
|
|
|
|
charger_icon = self._charger_icon.rotate(charger_position.a if self._robot_shape == 1 or self.icon_set == 2 or self.icon_set == 3 else (-map_rotation), expand=1)
|
|
|
|
point = charger_position.to_img(dimensions)
|
|
new_layer.paste(
|
|
charger_icon,
|
|
(int((point.x * scale) - (charger_icon.size[0] / 2)),
|
|
int((point.y * scale) - (charger_icon.size[1] / 2))),
|
|
charger_icon,
|
|
)
|
|
|
|
if robot_status > 5:
|
|
if self._robot_washing_icon is None:
|
|
self._robot_washing_icon = (
|
|
Image.open(
|
|
BytesIO(base64.b64decode(MAP_ROBOT_WASHING_IMAGE)))
|
|
.convert("RGBA")
|
|
.resize((int(icon_size * 1.25), int(icon_size * 1.25)), resample=Image.Resampling.NEAREST)
|
|
.rotate(-map_rotation)
|
|
)
|
|
enhancer = ImageEnhance.Brightness(self._robot_washing_icon)
|
|
if self.color_scheme.dark:
|
|
self._robot_washing_icon = enhancer.enhance(0.65)
|
|
|
|
icon = self._robot_washing_icon
|
|
|
|
icon_x = point.x * scale
|
|
icon_y = point.y * scale
|
|
offset = (icon_size * 1.5)
|
|
if map_rotation == 90:
|
|
icon_x = icon_x + offset
|
|
elif map_rotation == 180:
|
|
icon_y = icon_y + offset
|
|
elif map_rotation == 270:
|
|
icon_x = icon_x - offset
|
|
else:
|
|
icon_y = icon_y - offset
|
|
|
|
new_layer.paste(
|
|
icon,
|
|
(int(icon_x - (icon.size[0] / 2)),
|
|
int(icon_y - (icon.size[1] / 2))),
|
|
icon,
|
|
)
|
|
return new_layer
|
|
|
|
def render_vacuum(
|
|
self, robot_position, robot_status, layer, dimensions, size, map_rotation, scale
|
|
):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
icon_size = int(size * scale)
|
|
if self._robot_icon is None:
|
|
robot_icon_size = icon_size
|
|
if self.icon_set == 2:
|
|
robot_icon_size = int(icon_size * 1.4)
|
|
if self._robot_shape == 2:
|
|
robot_image = MAP_ROBOT_MOP_IMAGE_MIJIA
|
|
elif self._robot_shape == 1:
|
|
robot_image = MAP_ROBOT_VSLAM_IMAGE_MIJIA
|
|
else:
|
|
robot_image = MAP_ROBOT_LIDAR_IMAGE_MIJIA
|
|
else:
|
|
if self._robot_shape == 2:
|
|
robot_image = MAP_ROBOT_MOP_IMAGE_DREAME
|
|
elif self._robot_shape == 1:
|
|
if self.icon_set == 3:
|
|
robot_image = MAP_ROBOT_VSLAM_IMAGE_DREAME_LIGHT
|
|
else:
|
|
robot_image = MAP_ROBOT_VSLAM_IMAGE_DREAME_DARK
|
|
else:
|
|
if self.icon_set == 3:
|
|
robot_image = MAP_ROBOT_LIDAR_IMAGE_DREAME_LIGHT
|
|
else:
|
|
robot_image = MAP_ROBOT_LIDAR_IMAGE_DREAME_DARK
|
|
|
|
self._robot_icon = (
|
|
Image.open(BytesIO(base64.b64decode(robot_image)))
|
|
.convert("RGBA")
|
|
.resize((robot_icon_size, robot_icon_size), resample=Image.Resampling.NEAREST)
|
|
)
|
|
|
|
if self._robot_shape != 2 and self.icon_set != 2 and self.icon_set != 3:
|
|
enhancer = ImageEnhance.Brightness(self._robot_icon)
|
|
if self.color_scheme.dark:
|
|
self._robot_icon = enhancer.enhance(1.5)
|
|
else:
|
|
self._robot_icon = enhancer.enhance(0.9)
|
|
|
|
icon = self._robot_icon.rotate(robot_position.a)
|
|
point = robot_position.to_img(dimensions)
|
|
|
|
status_icon = None
|
|
if robot_status == 1:
|
|
if self._robot_cleaning_icon is None:
|
|
self._robot_cleaning_icon = (
|
|
Image.open(
|
|
BytesIO(base64.b64decode(MAP_ROBOT_CLEANING_IMAGE)))
|
|
.convert("RGBA")
|
|
.resize(((int(icon_size * 1.25), int(icon_size * 1.25))), resample=Image.Resampling.NEAREST)
|
|
)
|
|
status_icon = self._robot_cleaning_icon
|
|
|
|
if self.config.cleaning_direction:
|
|
if self._robot_cleaning_direction_icon is None:
|
|
self._robot_cleaning_direction_icon = (
|
|
Image.open(
|
|
BytesIO(base64.b64decode(MAP_ROBOT_CLEANING_DIRECTION_IMAGE)))
|
|
.convert("RGBA")
|
|
.resize(((int(icon_size * 1.5), int(icon_size * 1.5))), resample=Image.Resampling.NEAREST)
|
|
)
|
|
|
|
ico = self._robot_cleaning_direction_icon.rotate(robot_position.a, expand=1)
|
|
|
|
offset = int(icon_size / 2)
|
|
x = point.x + offset * math.cos(-robot_position.a * math.pi / 180)
|
|
y = point.y + offset * math.sin(-robot_position.a * math.pi / 180)
|
|
new_layer.paste(ico,
|
|
(
|
|
int(x * scale - (ico.size[0] / 2)),
|
|
int(y * scale - (ico.size[1] / 2)),
|
|
)
|
|
)
|
|
elif robot_status == 2:
|
|
if self._robot_charging_icon is None:
|
|
self._robot_charging_icon = (
|
|
Image.open(
|
|
BytesIO(base64.b64decode(MAP_ROBOT_CHARGING_IMAGE)))
|
|
.convert("RGBA")
|
|
.resize(((int(icon_size * 1.3), int(icon_size * 1.3))), resample=Image.Resampling.NEAREST)
|
|
)
|
|
status_icon = self._robot_charging_icon
|
|
elif robot_status == 3 or robot_status == 5 or robot_status == 6:
|
|
if self._robot_warning_icon is None:
|
|
self._robot_warning_icon = (
|
|
Image.open(
|
|
BytesIO(base64.b64decode(MAP_ROBOT_WARNING_IMAGE)))
|
|
.convert("RGBA")
|
|
.resize(((int(icon_size * 1.3), int(icon_size * 1.3))), resample=Image.Resampling.NEAREST)
|
|
)
|
|
status_icon = self._robot_warning_icon
|
|
|
|
if status_icon:
|
|
mask = Image.new("L", status_icon.size, 0)
|
|
draw = ImageDraw.Draw(mask)
|
|
draw.ellipse((0, 0, status_icon.size[0], status_icon.size[1]), fill=255)
|
|
new_layer.paste(
|
|
status_icon,
|
|
(
|
|
int(point.x * scale - (status_icon.size[0] / 2)),
|
|
int(point.y * scale - (status_icon.size[1] / 2)),
|
|
),
|
|
mask
|
|
)
|
|
|
|
new_layer.paste(
|
|
icon,
|
|
(
|
|
int(point.x * scale - (icon.size[0] / 2)),
|
|
int(point.y * scale - (icon.size[1] / 2)),
|
|
),
|
|
icon,
|
|
)
|
|
|
|
if robot_status == 4 or robot_status == 5:
|
|
if self._robot_sleeping_icon is None:
|
|
sleeping_icon = (
|
|
Image.open(
|
|
BytesIO(base64.b64decode(MAP_ROBOT_SLEEPING_IMAGE)))
|
|
.convert("RGBA")
|
|
.rotate(-map_rotation, expand=1)
|
|
)
|
|
enhancer = ImageEnhance.Brightness(sleeping_icon)
|
|
if not self.color_scheme.dark:
|
|
sleeping_icon = enhancer.enhance(0.7)
|
|
|
|
self._robot_sleeping_icon = [
|
|
sleeping_icon.resize(((int(icon_size * 0.3), int(icon_size * 0.3))), resample=Image.Resampling.NEAREST),
|
|
sleeping_icon.resize(
|
|
((int(icon_size * 0.35), int(icon_size * 0.35))), resample=Image.Resampling.NEAREST),
|
|
]
|
|
|
|
for k in [[int(icon_size * 0.34), int(icon_size * 0.18), 0], [int(icon_size * 0.43), int(icon_size * 0.43), 1]]:
|
|
status_icon = self._robot_sleeping_icon[k[2]]
|
|
if map_rotation == 90:
|
|
x = point.x + k[1]
|
|
y = point.y + k[0]
|
|
elif map_rotation == 180:
|
|
x = point.x - k[0]
|
|
y = point.y + k[1]
|
|
elif map_rotation == 270:
|
|
x = point.x - k[1]
|
|
y = point.y - k[0]
|
|
else:
|
|
x = point.x + k[0]
|
|
y = point.y - k[1]
|
|
|
|
new_layer.paste(
|
|
status_icon,
|
|
(
|
|
int(x * scale - (status_icon.size[0] / 2)),
|
|
int(y * scale - (status_icon.size[1] / 2)),
|
|
),
|
|
status_icon,
|
|
)
|
|
return new_layer
|
|
|
|
def render_segment(
|
|
self, segment, cleanset, layer, dimensions, size, rotation, scale
|
|
):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
draw = ImageDraw.Draw(new_layer, "RGBA")
|
|
if segment.x is not None and segment.y is not None:
|
|
text = None
|
|
icon = self._segment_icons.get(segment.type) if self.config.icon else None
|
|
if segment.type == 0 or icon is None:
|
|
text = segment.name if (self._robot_shape != 1 or icon is not None) or segment.custom_name is not None else segment.letter if self.icon_set != 2 else None
|
|
elif segment.index > 0:
|
|
text = str(segment.index)
|
|
|
|
text_font = None
|
|
order_font = None
|
|
if text and self.config.name:
|
|
text_font = ImageFont.truetype(
|
|
BytesIO(self.font_file),
|
|
int((size * 1.9)) if segment.index or icon is None else int((size * 1.7)),
|
|
)
|
|
|
|
if segment.order and self.config.order:
|
|
order_font = ImageFont.truetype(
|
|
BytesIO(self.font_file), int((size * 2.1))
|
|
)
|
|
|
|
p = Point(segment.x, segment.y).to_img(dimensions)
|
|
x = p.x
|
|
y = p.y
|
|
|
|
if self.config.name or self.config.icon:
|
|
if segment.type or text_font or not self.config.name:
|
|
icon_size = size * (1.75 if self.icon_set == 1 else 1.3)
|
|
x0 = x - size
|
|
y0 = y - size
|
|
x1 = x + size
|
|
y1 = y + size
|
|
|
|
if text_font:
|
|
left, top, tw, th = draw.textbbox((0, 0), text, text_font)
|
|
ws = tw / 4
|
|
|
|
if segment.index > 0 or icon is None:
|
|
icon_size = size * 1.35
|
|
padding = icon_size / 2
|
|
text_offset = (icon_size / 2) + 2
|
|
icon_offset = 2
|
|
th = int(size * 2.3)
|
|
else:
|
|
icon_size = size * 1.15
|
|
padding = icon_size * 0.35
|
|
icon_offset = padding - 2
|
|
text_offset = icon_size / 2
|
|
th = int(size * 1.9)
|
|
|
|
if icon is None:
|
|
text_offset = 0
|
|
padding = -(icon_size / 4)
|
|
|
|
if rotation == 90 or rotation == 270:
|
|
y0 = y0 - ws - padding
|
|
y1 = y1 + ws + padding
|
|
|
|
if rotation == 90:
|
|
ty = (y - ws + text_offset) * scale
|
|
tx = (x - (th / 4)) * scale
|
|
y = y - ws - icon_offset
|
|
else:
|
|
ty = (y - ws - text_offset) * scale
|
|
tx = (x - (th / 4)) * scale
|
|
y = y + ws + icon_offset
|
|
else:
|
|
x0 = x0 - ws - padding
|
|
x1 = x1 + ws + padding
|
|
|
|
if rotation == 0:
|
|
tx = (x - ws + text_offset) * scale
|
|
ty = (y - (th / 4)) * scale
|
|
x = x - ws - icon_offset
|
|
else:
|
|
tx = (x - ws - text_offset) * scale
|
|
ty = (y - (th / 4)) * scale
|
|
x = x + ws + icon_offset
|
|
|
|
if self.config.icon:
|
|
draw.rounded_rectangle(
|
|
[
|
|
int(x0 * scale),
|
|
int(y0 * scale),
|
|
int(x1 * scale),
|
|
int(y1 * scale),
|
|
],
|
|
fill=self.color_scheme.icon_background,
|
|
radius=((size * scale)),
|
|
)
|
|
|
|
icon_text = Image.new(
|
|
"RGBA", (tw, th), (255, 255, 255, 0))
|
|
draw_text = ImageDraw.Draw(icon_text, "RGBA")
|
|
|
|
if self.config.icon:
|
|
stroke_width = 1
|
|
text_color = self.color_scheme.text
|
|
stroke_color = self.color_scheme.text_stroke
|
|
else:
|
|
stroke_width = 4
|
|
if self.color_scheme.dark:
|
|
text_color = (240, 240, 240, 255)
|
|
stroke_color = (0, 0, 0, 210)
|
|
else:
|
|
text_color = (15, 15, 15, 255)
|
|
stroke_color = (255, 255, 255, 210)
|
|
|
|
draw_text.text(
|
|
(0, 0),
|
|
text,
|
|
font=text_font,
|
|
fill=text_color,
|
|
stroke_width=stroke_width,
|
|
stroke_fill=stroke_color,
|
|
)
|
|
icon_text = icon_text.rotate(-rotation, expand=1)
|
|
new_layer.paste(
|
|
icon_text, (int(tx), int(ty)), icon_text)
|
|
elif icon is not None:
|
|
draw.ellipse(
|
|
[x0 * scale, y0 * scale, x1 * scale, y1 * scale],
|
|
fill=self.color_scheme.icon_background,
|
|
)
|
|
|
|
if icon is not None:
|
|
s = icon_size * scale
|
|
icon = icon.resize((int(s), int(s))).rotate(-rotation, expand=1)
|
|
new_layer.paste(
|
|
icon, (int(x * scale - (icon.size[0] / 2)),
|
|
int(y * scale - (icon.size[1] / 2))), icon
|
|
)
|
|
|
|
custom = (
|
|
cleanset
|
|
and (self.config.suction_level or self.config.water_volume or self.config.cleaning_times or self.config.cleaning_mode)
|
|
)
|
|
if order_font or custom:
|
|
offset = size * 2.7
|
|
x_offset = 0
|
|
y_offset = -offset
|
|
|
|
if rotation == 90:
|
|
y_offset = 0
|
|
x_offset = offset
|
|
elif rotation == 180:
|
|
y_offset = offset
|
|
elif rotation == 270:
|
|
y_offset = 0
|
|
x_offset = -offset
|
|
|
|
x = p.x + x_offset
|
|
y = p.y + y_offset
|
|
cleaning_mode = None if segment.cleaning_mode is None or segment.cleaning_mode < 0 or segment.cleaning_mode > 3 else segment.cleaning_mode
|
|
if custom:
|
|
s = scale * 2
|
|
arrow = (s + 2) * scale
|
|
if order_font:
|
|
icon_count = 5
|
|
else:
|
|
icon_count = 4
|
|
if not self.config.suction_level or segment.suction_level is None:
|
|
icon_count = icon_count - 1
|
|
if not self.config.water_volume or segment.water_volume is None:
|
|
icon_count = icon_count - 1
|
|
if not self.config.cleaning_times or segment.cleaning_times is None:
|
|
icon_count = icon_count - 1
|
|
if not self.config.cleaning_mode or cleaning_mode is None:
|
|
icon_count = icon_count - 1
|
|
if cleaning_mode == 0 or cleaning_mode == 1:
|
|
icon_count = icon_count - 1
|
|
else:
|
|
icon_count = 1
|
|
|
|
if not icon and not self.config.icon:
|
|
arrow = 0
|
|
|
|
radius = size
|
|
arrow = int(round(radius * 0.6))
|
|
s = int(round(radius * 0.25))
|
|
margin = s if icon_count > 1 else 0
|
|
if custom:
|
|
radius = size - 2
|
|
|
|
icon_w = (
|
|
((radius * icon_count * 2) * scale) +
|
|
(arrow * 2) + (margin * 2)
|
|
)
|
|
icon_h = ((radius * 2) * scale) + (arrow * 2)
|
|
icon = Image.new("RGBA", (icon_w, icon_h),
|
|
(255, 255, 255, 0))
|
|
icon_draw = ImageDraw.Draw(icon, "RGBA")
|
|
|
|
if arrow and (segment.type != 0 or text_font):
|
|
xx = icon_w / 2
|
|
yy = icon_h - 2
|
|
icon_draw.polygon(
|
|
[
|
|
(xx, yy),
|
|
(xx - arrow, yy - arrow),
|
|
(xx + arrow, yy - arrow),
|
|
],
|
|
fill=self.color_scheme.settings_background,
|
|
)
|
|
|
|
icon_draw.rounded_rectangle(
|
|
[arrow, arrow, icon_w - arrow, icon_h - arrow],
|
|
fill=self.color_scheme.settings_background,
|
|
radius=((icon_h - (arrow * 2)) / 2),
|
|
)
|
|
|
|
padding = s + arrow
|
|
r = icon_h - (padding * 2)
|
|
ellipse_x1 = padding + margin
|
|
ellipse_x2 = ellipse_x1 + r
|
|
if order_font:
|
|
icon_draw.ellipse(
|
|
[ellipse_x1, padding, ellipse_x2, icon_h - padding],
|
|
fill=self.color_scheme.segment[
|
|
segment.color_index
|
|
][1],
|
|
)
|
|
text = str(segment.order)
|
|
left, top, tw, th = icon_draw.textbbox((0, 0), text, order_font)
|
|
icon_draw.text(
|
|
(
|
|
(icon_h - tw) / 2 + margin,
|
|
(icon_h - th - int(round(radius * 0.4))) / 2,
|
|
),
|
|
text,
|
|
font=order_font,
|
|
fill=self.color_scheme.order,
|
|
stroke_width=1,
|
|
stroke_fill=self.color_scheme.text_stroke,
|
|
)
|
|
|
|
ellipse_x1 = ellipse_x2 + (margin * 2)
|
|
ellipse_x2 = ellipse_x1 + r
|
|
|
|
if custom:
|
|
icon_size = size * 1.45
|
|
|
|
if self.config.cleaning_mode and cleaning_mode is not None:
|
|
if self.icon_set == 2:
|
|
s = icon_size * 1.2 * scale
|
|
else:
|
|
s = icon_size * 0.85 * scale
|
|
|
|
ico = DreameVacuumMapRenderer._set_icon_color(
|
|
self._cleaning_mode_icon[segment.cleaning_mode],
|
|
s,
|
|
self.color_scheme.segment[segment.color_index][
|
|
1
|
|
],
|
|
)
|
|
|
|
icon_draw.ellipse(
|
|
[ellipse_x1, padding, ellipse_x2,
|
|
(icon_h - padding)],
|
|
fill=self.color_scheme.settings_icon_background,
|
|
)
|
|
icon.paste(
|
|
ico,
|
|
(
|
|
int(
|
|
2
|
|
+ ellipse_x1
|
|
+ ((ellipse_x2 - ellipse_x1) / 2)
|
|
- ico.size[0] / 2
|
|
),
|
|
int(((icon_h / 2) - ico.size[1] / 2)),
|
|
),
|
|
ico,
|
|
)
|
|
|
|
ellipse_x1 = ellipse_x2 + (margin * 2)
|
|
ellipse_x2 = ellipse_x1 + r
|
|
|
|
if self.config.suction_level and segment.suction_level is not None and cleaning_mode != 1:
|
|
if self.icon_set == 2:
|
|
s = icon_size * 1.2 * scale
|
|
else:
|
|
s = icon_size * 0.85 * scale
|
|
|
|
ico = DreameVacuumMapRenderer._set_icon_color(
|
|
self._suction_level_icon[segment.suction_level],
|
|
s,
|
|
self.color_scheme.segment[segment.color_index][
|
|
1
|
|
],
|
|
)
|
|
icon_draw.ellipse(
|
|
[ellipse_x1, padding, ellipse_x2,
|
|
(icon_h - padding)],
|
|
fill=self.color_scheme.settings_icon_background,
|
|
)
|
|
icon.paste(
|
|
ico,
|
|
(
|
|
int(
|
|
2
|
|
+ ellipse_x1
|
|
+ ((ellipse_x2 - ellipse_x1) / 2)
|
|
- ico.size[0] / 2
|
|
),
|
|
int(((icon_h / 2) - ico.size[1] / 2)),
|
|
),
|
|
ico,
|
|
)
|
|
|
|
ellipse_x1 = ellipse_x2 + (margin * 2)
|
|
ellipse_x2 = ellipse_x1 + r
|
|
|
|
if self.config.water_volume and segment.water_volume is not None and cleaning_mode != 0:
|
|
if self.icon_set == 3:
|
|
s = icon_size * 0.95 * scale
|
|
elif self.icon_set == 2:
|
|
s = icon_size * 1.2 * scale
|
|
else:
|
|
s = icon_size * 0.85 * scale
|
|
|
|
ico = DreameVacuumMapRenderer._set_icon_color(
|
|
self._water_volume_icon[segment.water_volume - 1],
|
|
s,
|
|
self.color_scheme.segment[segment.color_index][
|
|
1
|
|
],
|
|
)
|
|
|
|
icon_draw.ellipse(
|
|
[ellipse_x1, padding, ellipse_x2,
|
|
(icon_h - padding)],
|
|
fill=self.color_scheme.settings_icon_background,
|
|
)
|
|
icon.paste(
|
|
ico,
|
|
(
|
|
int(
|
|
2
|
|
+ ellipse_x1
|
|
+ ((ellipse_x2 - ellipse_x1) / 2)
|
|
- ico.size[0] / 2
|
|
),
|
|
int(((icon_h / 2) - ico.size[1] / 2)),
|
|
),
|
|
ico,
|
|
)
|
|
|
|
ellipse_x1 = ellipse_x2 + (margin * 2)
|
|
ellipse_x2 = ellipse_x1 + r
|
|
|
|
if self.config.cleaning_times and segment.cleaning_times is not None:
|
|
if self.icon_set == 3 or self.icon_set == 2:
|
|
s = icon_size * 0.95 * scale
|
|
else:
|
|
s = icon_size * 0.85 * scale
|
|
|
|
ico = DreameVacuumMapRenderer._set_icon_color(
|
|
self._cleaning_times_icon[segment.cleaning_times - 1],
|
|
s,
|
|
self.color_scheme.segment[segment.color_index][
|
|
1
|
|
],
|
|
)
|
|
|
|
icon_draw.ellipse(
|
|
[ellipse_x1, padding, ellipse_x2,
|
|
(icon_h - padding)],
|
|
fill=self.color_scheme.settings_icon_background,
|
|
)
|
|
icon.paste(
|
|
ico,
|
|
(
|
|
int(
|
|
2
|
|
+ ellipse_x1
|
|
+ ((ellipse_x2 - ellipse_x1) / 2)
|
|
- ico.size[0] / 2
|
|
),
|
|
int(((icon_h / 2) - ico.size[1] / 2)),
|
|
),
|
|
ico,
|
|
)
|
|
|
|
icon = icon.rotate(-rotation, expand=1)
|
|
new_layer.paste(
|
|
icon,
|
|
(
|
|
int((x * scale) - ((icon.size[0]) / 2)),
|
|
int((y * scale) - ((icon.size[1]) / 2)),
|
|
),
|
|
icon,
|
|
)
|
|
return new_layer
|
|
|
|
def render_obstacles(self, obstacles, layer, dimensions, size, rotation, scale):
|
|
new_layer = Image.new("RGBA", layer.size, (255, 255, 255, 0))
|
|
icon_size = (size * scale * 0.85)
|
|
draw = ImageDraw.Draw(new_layer, "RGBA")
|
|
|
|
if self._obstacle_background is None:
|
|
self._obstacle_background = (
|
|
Image.open(BytesIO(base64.b64decode(MAP_ICON_OBSTACLE_BG_DREAME)))
|
|
.convert("RGBA")
|
|
.rotate(-rotation)
|
|
)
|
|
self._obstacle_background.thumbnail(
|
|
(size * scale * scale, size * scale * scale), Image.Resampling.LANCZOS)
|
|
|
|
bg_size = int(round((size * scale * 0.5) / 2))
|
|
offset = -8 * scale
|
|
if rotation == 90:
|
|
y_offset = 0
|
|
x_offset = offset
|
|
elif rotation == 180:
|
|
y_offset = offset
|
|
x_offset = 0
|
|
elif rotation == 270:
|
|
y_offset = 0
|
|
x_offset = -offset
|
|
else:
|
|
x_offset = 0
|
|
y_offset = -offset
|
|
|
|
for obstacle in obstacles:
|
|
icon = self._obstacle_icons.get(obstacle.obstacle_type.value)
|
|
if icon:
|
|
p = obstacle.to_img(dimensions)
|
|
x = p.x
|
|
y = p.y
|
|
|
|
new_layer.paste(
|
|
self._obstacle_background, (int(round(x * scale - (self._obstacle_background.size[0] / 2) + x_offset)),
|
|
int(round(y * scale - (self._obstacle_background.size[1] / 2) + y_offset)))
|
|
)
|
|
|
|
draw.ellipse(
|
|
[(x - bg_size) * scale, (y - bg_size) * scale,
|
|
(x + bg_size) * scale, (y + bg_size) * scale],
|
|
fill=self.color_scheme.segment[0][0],
|
|
)
|
|
|
|
icon = icon.resize(
|
|
(int(icon_size), int(icon_size))).rotate(-rotation)
|
|
new_layer.paste(
|
|
icon, (int(round(x * scale - (icon_size / 2))),
|
|
int(round(y * scale - (icon_size / 2)))), icon
|
|
)
|
|
|
|
return new_layer
|
|
|
|
@property
|
|
def calibration_points(self) -> dict[str, int]:
|
|
return self._calibration_points
|
|
|
|
@property
|
|
def default_map_image(self) -> bytes:
|
|
return self._to_buffer(self._default_map_image)
|
|
|
|
@property
|
|
def disconnected_map_image(self) -> bytes:
|
|
if self._image:
|
|
return self._to_buffer(self._image.filter(ImageFilter.GaussianBlur(13)))
|
|
return self.default_map_image
|
|
|
|
@property
|
|
def default_calibration_points(self) -> dict[str, int]:
|
|
return self._default_calibration_points
|
|
|
|
|
|
|
|
class DreameVacuumMapOptimizer:
|
|
def __init__(self) -> None:
|
|
self._js_optimizer = None
|
|
|
|
def _clean_wall(self, data, width, height):
|
|
for j in range(1, height - 1):
|
|
for i in range(1, width - 1):
|
|
index = j * width + i
|
|
if data[index] == 1:
|
|
num = 0
|
|
if data[index - 1] != 1:
|
|
num = num + 1
|
|
if data[index + 1] != 1:
|
|
num = num + 1
|
|
if data[index + width] != 1:
|
|
num = num + 1
|
|
if data[index - width] != 1:
|
|
num = num + 1
|
|
if num > 2:
|
|
data[index] = 0
|
|
|
|
for j in range(1, height - 1):
|
|
for i in range(1, width - 1):
|
|
index = j * width + i
|
|
if data[index] == 2:
|
|
if (data[index - 1] == 1 and data[index + 1] == 1) or (data[index + width] == 1 and data[index - width] == 1):
|
|
data[index] = 1
|
|
|
|
for i in range(len(data)):
|
|
if data[i] == 2:
|
|
data[i] = 0
|
|
|
|
def _obstacle_data(self, data, width, height):
|
|
for it in range(2):
|
|
for j in range(height):
|
|
for i in range(width):
|
|
index = j * width + i
|
|
cValue = data[index]
|
|
if cValue == 2:
|
|
l = (0 if i == 0 else data[index - 1])
|
|
r = (0 if i == (width - 1) else data[index + 1])
|
|
t = (0 if j == (height - 1) else data[index + width])
|
|
b = (0 if j == 0 else data[index - width])
|
|
if (l == 0 and r == 2) or (l == 2 and r == 0) or (t == 0 and b == 2) or (t == 2 and b == 0):
|
|
data[index] = 0
|
|
|
|
def _find_first_empty_point(self, data, width, height):
|
|
size = len(data)
|
|
|
|
for i in range(width):
|
|
if data[i] == 0:
|
|
return [i, 0]
|
|
|
|
if data[(height - 1) * width + i] == 0:
|
|
return [i, (height - 1)]
|
|
|
|
for j in range(height):
|
|
if data[j * width] == 0:
|
|
return [0, j]
|
|
|
|
if data[j * width + (width - 1)] == 0:
|
|
return [(width - 1), j]
|
|
|
|
def _find_zero_point(self, data, width, height, point):
|
|
finds = []
|
|
x = point[0]
|
|
y = point[1]
|
|
for _j in range(y - 1, y + 2):
|
|
for _i in range(x - 1, x + 2):
|
|
if _j == y or _i == x:
|
|
index = _j * width + _i
|
|
if data[index] == 0:
|
|
data[index] = 255
|
|
finds.append([_i, _j])
|
|
return finds
|
|
|
|
def _fill_map_data(self, data, width, height, fill):
|
|
self._fill_map_data_2(data, width, height)
|
|
|
|
size = len(data)
|
|
ssize = 3
|
|
|
|
for it in range(2):
|
|
for i in range(width):
|
|
startY = -1
|
|
isEmpty = False
|
|
for j in range(height):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if isEmpty and startY >= 0:
|
|
if (j - startY - 1) <= ssize:
|
|
for _j in range(startY + 1, j):
|
|
num = 0
|
|
if i > 0 and _j > 0:
|
|
for __i in range(i - 1, i + 2):
|
|
for __j in range(_j - 1, _j + 2):
|
|
if __i != i and __j != _j:
|
|
if __i == i or __j == _j:
|
|
ind = __j * width + __i
|
|
if ind >= 0 and ind < size and data[__j * width + __i] != 0:
|
|
num = num + 1
|
|
else:
|
|
num = 5
|
|
|
|
if num >= 3:
|
|
data[_j * width + i] = fill
|
|
|
|
isEmpty = False
|
|
startY = j
|
|
else:
|
|
if startY >= 0:
|
|
isEmpty = True
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
isEmpty = False
|
|
for i in range(width):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if isEmpty and startX >= 0:
|
|
if (i - startX - 1) <= ssize:
|
|
for _i in range(startX + 1, i):
|
|
num = 0
|
|
if _i > 0 and j > 0:
|
|
for __i in range(_i - 1, _i + 2):
|
|
for __j in range(j - 1, j + 2):
|
|
if __i != _i and __j != j:
|
|
if __i == _i or __j == j:
|
|
ind = __j * width + __i
|
|
if ind >= 0 and ind < size and data[__j * width + __i] != 0:
|
|
num = num + 1
|
|
else:
|
|
num = 5
|
|
|
|
if num >= 3:
|
|
data[j * width + _i] = fill
|
|
|
|
isEmpty = False
|
|
|
|
startX = i
|
|
else:
|
|
if startX >= 0:
|
|
isEmpty = True
|
|
|
|
def _denoise(self, data, width, height):
|
|
tmpMapInfo = data.copy()
|
|
ssize = 20
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if startY < 0:
|
|
startY = j
|
|
continue
|
|
|
|
if startY != -1 and (j - startY) <= ssize:
|
|
isBorder = False
|
|
if i == 0 or i == (width - 1) or (j - startY) <= 2:
|
|
isBorder = True
|
|
|
|
if not isBorder:
|
|
_i = i - 1
|
|
isBorder = True
|
|
for k in range(startY, j):
|
|
if tmpMapInfo[k * width + _i] == 1:
|
|
isBorder = False
|
|
break
|
|
|
|
if not isBorder:
|
|
_i = i + 1
|
|
isBorder = True
|
|
for k in range(startY, j):
|
|
if tmpMapInfo[k * width + _i] == 1:
|
|
isBorder = False
|
|
break
|
|
|
|
if isBorder:
|
|
for k in range(startY, j):
|
|
data[k * width + i] = 0
|
|
|
|
startY = -1
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if startX < 0:
|
|
startX = i
|
|
continue
|
|
|
|
if startX != -1 and (i - startX) <= ssize:
|
|
isBorder = False
|
|
if j == 0 or j == (height - 1) or (i - startX) <= 2:
|
|
isBorder = True
|
|
|
|
if not isBorder:
|
|
_j = j - 1
|
|
isBorder = True
|
|
for k in range(startX, i):
|
|
if tmpMapInfo[_j * width + k] == 1:
|
|
isBorder = False
|
|
break
|
|
|
|
if not isBorder:
|
|
_j = j + 1
|
|
isBorder = True
|
|
for k in range(startX, i):
|
|
if tmpMapInfo[_j * width + k] == 1:
|
|
isBorder = False
|
|
break
|
|
|
|
if isBorder:
|
|
for k in range(startX, i):
|
|
data[j * width + k] = 0
|
|
|
|
startX = -1
|
|
|
|
ssize = 2
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if startY < 0:
|
|
startY = j
|
|
continue
|
|
|
|
if startY != -1 and (j - startY) <= ssize:
|
|
for k in range(startY, j):
|
|
data[k * width + i] = 0
|
|
|
|
startY = -1
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if startX < 0:
|
|
startX = i
|
|
continue
|
|
|
|
if startX != -1 and (i - startX) <= ssize:
|
|
for k in range(startX, i):
|
|
data[j * width + k] = 0
|
|
|
|
startX = -1
|
|
|
|
def _update_border_value(self, data, width, height, stroke):
|
|
for j in range(height):
|
|
for i in range(width):
|
|
index = j * width + i
|
|
if data[index] != 0:
|
|
if j == 0 or j == (height - 1) or i == 0 or i == (width - 1):
|
|
data[index] = stroke
|
|
else:
|
|
hasFind = False
|
|
for _i in range(i - 1, i + 2):
|
|
for _j in range(j - 1, j + 2):
|
|
if data[_j * width + _i] == 0:
|
|
hasFind = True
|
|
break
|
|
if hasFind:
|
|
break
|
|
|
|
if hasFind:
|
|
data[index] = stroke
|
|
|
|
def _fill_cross_line(self, data, width, height, stroke):
|
|
size = len(data)
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
lastY = j - 1
|
|
if data[index] == stroke and j != (height - 1):
|
|
if startY < 0:
|
|
startY = j
|
|
continue
|
|
|
|
if startY >= 0:
|
|
if j == (height - 1) and data[index] == stroke:
|
|
lastY = j
|
|
|
|
if lastY == startY:
|
|
startY = -1
|
|
continue
|
|
|
|
crossNum = 0
|
|
for _j in range(startY, lastY + 1):
|
|
_i = i - 1
|
|
if _i >= 0:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == stroke:
|
|
crossNum = crossNum + 1
|
|
|
|
_i = i + 1
|
|
if _i < width:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == stroke:
|
|
crossNum = crossNum + 1
|
|
|
|
if crossNum > 2:
|
|
break
|
|
|
|
if crossNum > 2:
|
|
for _j in range(startY, lastY + 1):
|
|
_i = i - 1
|
|
if _i >= 0:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == 0:
|
|
data[cIndex] = 1
|
|
|
|
_i = i + 1
|
|
if _i < width:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == 0:
|
|
data[cIndex] = 1
|
|
|
|
startY = -1
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
lastX = i - 1
|
|
if data[index] == stroke and i != (width - 1):
|
|
if startX < 0:
|
|
startX = i
|
|
continue
|
|
|
|
if startX >= 0:
|
|
if data[index] == stroke and i == (width - 1):
|
|
lastX = i
|
|
|
|
if lastX == startX:
|
|
startX = -1
|
|
continue
|
|
|
|
crossNum = 0
|
|
for _i in range(startX, lastX + 1):
|
|
_j = j - 1
|
|
if _j >= 0:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == stroke:
|
|
crossNum = crossNum + 1
|
|
|
|
_j = j + 1
|
|
if _j < width:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == stroke:
|
|
crossNum = crossNum + 1
|
|
|
|
if crossNum > 2:
|
|
break
|
|
|
|
if crossNum > 2:
|
|
for _i in range(startX, lastX + 1):
|
|
_j = j - 1
|
|
if _j >= 0:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == 0:
|
|
data[cIndex] = 1
|
|
|
|
_j = j + 1
|
|
if _j < width:
|
|
cIndex = _j * width + _i
|
|
if cIndex < size and data[cIndex] == 0:
|
|
data[cIndex] = 1
|
|
|
|
startX = -1
|
|
|
|
for i in range(len(data)):
|
|
if data[i] == stroke:
|
|
data[i] = 1
|
|
|
|
self._update_border_value(
|
|
data, width, height, stroke)
|
|
|
|
def _check_intersect(self, arr1, arr2) -> list[int]:
|
|
if arr1[0] >= arr2[1] or arr2[0] >= arr1[1]:
|
|
return None
|
|
|
|
def sort_data(a, b):
|
|
return a - b
|
|
|
|
tmp = arr1 + arr2
|
|
tmp.sort(key=cmp_to_key(sort_data))
|
|
return [tmp[1], tmp[2]]
|
|
|
|
def _find_original_points(self, original_data, data, width, xs, ys) -> float:
|
|
if xs[0] > xs[1]:
|
|
tmp = xs[0]
|
|
xs[0] = xs[1]
|
|
xs[1] = tmp
|
|
|
|
if ys[0] > ys[1]:
|
|
tmp = ys[0]
|
|
ys[0] = ys[1]
|
|
ys[1] = tmp
|
|
|
|
num = 0
|
|
for i in range(xs[0], xs[1] + 1):
|
|
for j in range(ys[0], ys[1] + 1):
|
|
value = original_data[j * width + i]
|
|
if value != 0:
|
|
num = num + 1
|
|
|
|
weight = num / ((xs[1] - xs[0] + 1) * (ys[1] - ys[0] + 1))
|
|
if weight > 0.5:
|
|
size = len(data)
|
|
for i in range(xs[0], xs[1] + 1):
|
|
for j in range(ys[0], ys[1] + 1):
|
|
nIndex = j * width + i
|
|
if nIndex < size:
|
|
data[nIndex] = 1
|
|
return weight
|
|
|
|
def _add_line(self, line, covertlines, allLines):
|
|
aLine = ALine()
|
|
if line.ishorizontal:
|
|
aLine.p0.y = line.y
|
|
aLine.p1.y = line.y
|
|
if line.findEnd:
|
|
aLine.p0.x = line.x[0]
|
|
aLine.p1.x = line.x[1]
|
|
else:
|
|
aLine.p0.x = line.x[1]
|
|
aLine.p1.x = line.x[0]
|
|
aLine.length = abs(line.x[1] - line.x[0])
|
|
else:
|
|
aLine.p0.x = line.x
|
|
aLine.p1.x = line.x
|
|
aLine.length = abs(line.y[1] - line.y[0])
|
|
if line.findEnd:
|
|
aLine.p0.y = line.y[0]
|
|
aLine.p1.y = line.y[1]
|
|
else:
|
|
aLine.p0.y = line.y[1]
|
|
aLine.p1.y = line.y[0]
|
|
covertlines.append(aLine)
|
|
allLines.append(line)
|
|
|
|
def _find_bounds(self, data, width, horizontalLines, verticalLines) -> list[Paths]:
|
|
paths = []
|
|
size = len(data)
|
|
|
|
while horizontalLines:
|
|
startLine = horizontalLines.pop(0)
|
|
startLine.findEnd = True
|
|
covertlines = []
|
|
allLines = []
|
|
self._add_line(startLine, covertlines, allLines)
|
|
while True:
|
|
lastLine = allLines[len(allLines) - 1]
|
|
if lastLine.ishorizontal:
|
|
hasFind = False
|
|
|
|
lines = verticalLines.copy()
|
|
for i in range(len(lines)):
|
|
vLine = lines[i]
|
|
|
|
x = lastLine.x[0]
|
|
if lastLine.findEnd:
|
|
x = lastLine.x[1]
|
|
|
|
if x == vLine.x:
|
|
if lastLine.y == vLine.y[0]:
|
|
vLine.findEnd = True
|
|
self._add_line(
|
|
vLine, covertlines, allLines)
|
|
del verticalLines[i]
|
|
hasFind = True
|
|
break
|
|
elif lastLine.y == vLine.y[1]:
|
|
vLine.findEnd = False
|
|
self._add_line(
|
|
vLine, covertlines, allLines)
|
|
del verticalLines[i]
|
|
hasFind = True
|
|
break
|
|
elif lastLine.y > vLine.y[0] and lastLine.y < vLine.y[1]:
|
|
if lastLine.findEnd:
|
|
nIndex = (lastLine.y + 1) * width + x - 1
|
|
if nIndex < size and data[nIndex] == 0:
|
|
vLine.y[1] = lastLine.y
|
|
vLine.findEnd = False
|
|
else:
|
|
vLine.y[0] = lastLine.y
|
|
vLine.findEnd = True
|
|
else:
|
|
nIndex = (lastLine.y + 1) * width + x + 1
|
|
if nIndex < size and data[nIndex] == 0:
|
|
vLine.y[1] = lastLine.y
|
|
vLine.findEnd = False
|
|
else:
|
|
vLine.y[0] = lastLine.y
|
|
vLine.findEnd = True
|
|
|
|
self._add_line(
|
|
vLine, covertlines, allLines)
|
|
del verticalLines[i]
|
|
hasFind = True
|
|
break
|
|
|
|
if not hasFind:
|
|
break
|
|
else:
|
|
hasFind = False
|
|
_y = lastLine.y[0]
|
|
if lastLine.findEnd:
|
|
_y = lastLine.y[1]
|
|
|
|
if _y == startLine.y and lastLine.x == startLine.x[0]:
|
|
break
|
|
|
|
lines = horizontalLines.copy()
|
|
for i in range(len(lines)):
|
|
hLine = lines[i]
|
|
|
|
y = lastLine.y[0]
|
|
if lastLine.findEnd:
|
|
y = lastLine.y[1]
|
|
|
|
if y == hLine.y:
|
|
if lastLine.x == hLine.x[0]:
|
|
hLine.findEnd = True
|
|
self._add_line(
|
|
hLine, covertlines, allLines)
|
|
del horizontalLines[i]
|
|
hasFind = True
|
|
break
|
|
elif lastLine.x == hLine.x[1]:
|
|
hLine.findEnd = False
|
|
self._add_line(
|
|
hLine, covertlines, allLines)
|
|
del horizontalLines[i]
|
|
hasFind = True
|
|
break
|
|
elif lastLine.x > hLine.x[0] and lastLine.x < hLine.x[1]:
|
|
if lastLine.findEnd:
|
|
nIndex = (y - 1) * width + lastLine.x - 1
|
|
if nIndex < size and data[nIndex] == 0:
|
|
hLine.x[0] = lastLine.x
|
|
hLine.findEnd = True
|
|
else:
|
|
hLine.x[1] = lastLine.x
|
|
hLine.findEnd = False
|
|
else:
|
|
nIndex = (y + 1) * width + lastLine.x - 1
|
|
if nIndex < size and data[nIndex] == 0:
|
|
hLine.x[0] = lastLine.x
|
|
hLine.findEnd = True
|
|
else:
|
|
hLine.x[1] = lastLine.x
|
|
hLine.findEnd = False
|
|
|
|
self._add_line(
|
|
hLine, covertlines, allLines)
|
|
del horizontalLines[i]
|
|
hasFind = True
|
|
break
|
|
|
|
if not hasFind:
|
|
break
|
|
|
|
totalLength = 0
|
|
for i in range(len(covertlines)):
|
|
item = covertlines[i]
|
|
totalLength = totalLength + item.length
|
|
|
|
paths.append(Paths(
|
|
clines = covertlines,
|
|
alines = allLines,
|
|
length = totalLength
|
|
))
|
|
|
|
return paths
|
|
|
|
def _fill_map_data_2(self, data, width, height):
|
|
while True:
|
|
first_point = self._find_first_empty_point(
|
|
data, width, height)
|
|
if first_point is None:
|
|
break
|
|
|
|
data[first_point[1] * width + first_point[0]] = 255
|
|
needFindPoints = [first_point]
|
|
while needFindPoints:
|
|
needFindPoints.extend(self._find_zero_point(data, width, height, needFindPoints.pop(0)))
|
|
|
|
for i in range(len(data)):
|
|
if data[i] == 0:
|
|
data[i] = 3
|
|
elif data[i] == 255:
|
|
data[i] = 0
|
|
|
|
def _link_adjacent_areas(self, original_data, data, width, height, stroke):
|
|
horizontalLines = []
|
|
verticalLines = []
|
|
DIR_LEFT = 1
|
|
DIR_RIGHT = 2
|
|
DIR_TOP = 3
|
|
DIR_BOTTOM = 4
|
|
size = len(data)
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
lastY = j - 1
|
|
if data[index] == stroke and j != height - 1:
|
|
isCross = False
|
|
if (i != 0 and data[index - 1] == stroke) or (i != (width - 1) and data[index + 1] == stroke):
|
|
isCross = True
|
|
if startY < 0 and isCross:
|
|
startY = j
|
|
continue
|
|
|
|
if not isCross:
|
|
continue
|
|
|
|
lastY = j
|
|
|
|
if startY >= 0:
|
|
if j == (height - 1) and data[index] == stroke:
|
|
lastY = j
|
|
|
|
if lastY == startY:
|
|
startY = -1
|
|
continue
|
|
|
|
isCross = False
|
|
direction = DIR_LEFT
|
|
lastIndex = lastY * width + i
|
|
if data[lastIndex - 1] == stroke or data[lastIndex + 1] == stroke:
|
|
isCross = True
|
|
|
|
if i == 0:
|
|
direction = DIR_LEFT
|
|
elif i == (width - 1):
|
|
direction = DIR_RIGHT
|
|
elif data[lastIndex - 1] == stroke:
|
|
if data[lastIndex + 1] != 0:
|
|
direction = DIR_LEFT
|
|
else:
|
|
direction = DIR_RIGHT
|
|
elif data[lastIndex + 1] == stroke:
|
|
if data[lastIndex - 1] != 0:
|
|
direction = DIR_RIGHT
|
|
else:
|
|
direction = DIR_LEFT
|
|
|
|
if isCross:
|
|
verticalLines.append(CLine(x = i, y = [startY, lastY], ishorizontal = False, direction = direction, length = (lastY - startY)))
|
|
startY = lastY
|
|
continue
|
|
startY = -1
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
lastX = i - 1
|
|
if data[index] == stroke and i != (width - 1):
|
|
isCross = False
|
|
nIndex = index - width
|
|
nnIndex = index + width
|
|
if data[nIndex] == stroke or data[nnIndex] == stroke:
|
|
isCross = True
|
|
if startX < 0 and isCross:
|
|
startX = i
|
|
continue
|
|
if not isCross:
|
|
continue
|
|
|
|
lastX = i
|
|
|
|
if startX >= 0:
|
|
if data[index] == stroke and i == (width - 1):
|
|
lastX = i
|
|
|
|
if lastX == startX:
|
|
startX = -1
|
|
continue
|
|
|
|
isCross = False
|
|
direction = DIR_TOP
|
|
lastIndex = j * width + lastX
|
|
nIndex = lastIndex - width
|
|
nnIndex = lastIndex + width
|
|
if (nIndex >= 0 and data[nIndex] == stroke) or (nnIndex < size and data[nnIndex] == stroke):
|
|
isCross = True
|
|
|
|
if j == 0:
|
|
direction = DIR_BOTTOM
|
|
elif j == (height - 1):
|
|
direction = DIR_TOP
|
|
elif data[nIndex] == stroke:
|
|
if data[nnIndex] != 0:
|
|
direction = DIR_BOTTOM
|
|
else:
|
|
direction = DIR_TOP
|
|
elif data[nnIndex] == stroke:
|
|
if data[nIndex] != 0:
|
|
direction = DIR_TOP
|
|
else:
|
|
direction = DIR_BOTTOM
|
|
|
|
if isCross:
|
|
horizontalLines.append(CLine(x = [startX, lastX], y = j, ishorizontal = True, direction = direction, length = (lastX - startX)))
|
|
startX = lastX
|
|
continue
|
|
|
|
startX = -1
|
|
|
|
paths = self._find_bounds(data, width, horizontalLines, verticalLines)
|
|
needFill = len(paths) > 1
|
|
while len(paths) > 1:
|
|
lines = paths.pop(0).alines
|
|
|
|
for l in range(len(lines)):
|
|
line = lines[l]
|
|
for i in range(len(paths)):
|
|
nLines = paths[i].alines
|
|
for j in range(len(nLines)):
|
|
nLine = nLines[j]
|
|
if line.ishorizontal == False and nLine.ishorizontal == False:
|
|
if line.direction != nLine.direction:
|
|
if (line.x > nLine.x and line.direction == DIR_LEFT) or (line.x < nLine.x and line.direction == DIR_RIGHT):
|
|
if abs(line.x - nLine.x) <= 10:
|
|
_ys = self._check_intersect(
|
|
line.y, nLine.y)
|
|
if _ys != None:
|
|
xs = [line.x + 1, nLine.x - 1]
|
|
if line.x > nLine.x:
|
|
xs = [nLine.x + 1, line.x - 1]
|
|
weight = self._find_original_points(
|
|
original_data, data, width, xs, _ys)
|
|
elif line.ishorizontal == True and nLine.ishorizontal == True:
|
|
if line.direction != nLine.direction:
|
|
if (line.y > nLine.y and line.direction == DIR_BOTTOM) or (line.y < nLine.y and line.direction == DIR_TOP):
|
|
if abs(line.y - nLine.y) <= 10:
|
|
_xs = self._check_intersect(
|
|
line.x, nLine.x)
|
|
if _xs != None:
|
|
ys = [line.y + 1, nLine.y - 1]
|
|
if line.y > nLine.y:
|
|
ys = [nLine.y + 1, line.y - 1]
|
|
weight = self._find_original_points(
|
|
original_data, data, width, _xs, ys)
|
|
|
|
if needFill:
|
|
for i in range(len(data)):
|
|
if data[i] == stroke:
|
|
data[i] = 1
|
|
|
|
self._fill_map_data_2(data, width, height)
|
|
self._update_border_value(
|
|
data, width, height, stroke)
|
|
self._fill_cross_line(
|
|
data, width, height, stroke)
|
|
|
|
def _fill_angle(self, data, width, stroke, angle):
|
|
bottom = 5
|
|
right = 6
|
|
top = 7
|
|
left = 8
|
|
|
|
l1 = angle.lines[0]
|
|
l2 = angle.lines[len(angle.lines) - 1]
|
|
if len(angle.lines) == 2 or len(angle.lines) > 22:
|
|
nextAngle = Angle(lines = [l2])
|
|
if l2.ishorizontal:
|
|
nextAngle.horizontalDir = right if l2.findEnd else left
|
|
else:
|
|
nextAngle.verticalDir = top if l2.findEnd else bottom
|
|
return nextAngle
|
|
|
|
minx = None
|
|
miny = None
|
|
maxx = None
|
|
maxy = None
|
|
if l1.ishorizontal:
|
|
if angle.horizontalDir == right:
|
|
minx = l1.x[1]
|
|
else:
|
|
maxx = l1.x[0]
|
|
|
|
if angle.verticalDir == top:
|
|
miny = l1.y
|
|
else:
|
|
maxy = l1.y
|
|
|
|
if l2.ishorizontal:
|
|
if angle.horizontalDir == right:
|
|
maxx = l2.x[0]
|
|
else:
|
|
minx = l2.x[1]
|
|
|
|
if angle.verticalDir == top:
|
|
maxy = l2.y
|
|
else:
|
|
miny = l2.y
|
|
else:
|
|
if angle.horizontalDir == right:
|
|
maxx = l2.x
|
|
else:
|
|
minx = l2.x
|
|
if angle.verticalDir == top:
|
|
maxy = l2.y[0]
|
|
else:
|
|
miny = l2.y[1]
|
|
else:
|
|
if angle.verticalDir == top:
|
|
miny = l1.y[1]
|
|
else:
|
|
maxy = l1.y[0]
|
|
|
|
if angle.horizontalDir == right:
|
|
minx = l1.x
|
|
else:
|
|
maxx = l1.x
|
|
|
|
if l2.ishorizontal:
|
|
if angle.horizontalDir == right:
|
|
maxx = l2.x[0]
|
|
else:
|
|
minx = l2.x[1]
|
|
if angle.verticalDir == top:
|
|
maxy = l2.y
|
|
else:
|
|
miny = l2.y
|
|
|
|
else:
|
|
if angle.horizontalDir == right:
|
|
maxx = l2.x
|
|
else:
|
|
minx = l2.x
|
|
|
|
if angle.verticalDir == top:
|
|
maxy = l2.y[0]
|
|
else:
|
|
miny = l2.y[1]
|
|
|
|
if minx is None or miny is None or maxx is None or maxy is None:
|
|
nextAngle = Angle(lines = [l2])
|
|
if (l2.ishorizontal):
|
|
nextAngle.horizontalDir = right if l2.findEnd else left
|
|
else:
|
|
nextAngle.verticalDir = top if l2.findEnd else bottom
|
|
return nextAngle
|
|
|
|
if l1.ishorizontal and l2.ishorizontal and ((maxy - miny) <= 3):
|
|
if angle.horizontalDir == right:
|
|
minx = l1.x[0]
|
|
maxx = l2.x[1]
|
|
else:
|
|
minx = l2.x[0]
|
|
maxx = l1.x[1]
|
|
elif not l1.ishorizontal and not l2.ishorizontal and ((maxx - minx) <= 3):
|
|
if angle.verticalDir == top:
|
|
miny = l1.y[0]
|
|
maxy = l2.y[1]
|
|
else:
|
|
miny = l2.y[0]
|
|
maxy = l1.y[1]
|
|
|
|
num = 0
|
|
for i in range(minx, maxx + 1):
|
|
for j in range(miny, maxy + 1):
|
|
index = j * width + i
|
|
if data[index] == 0:
|
|
num = num + 1
|
|
|
|
if num < 20 or num < (((maxx - minx + 1) * (maxy - miny + 1) * 2) / 3):
|
|
for i in range(minx, maxx + 1):
|
|
for j in range(miny, maxy + 1):
|
|
index = j * width + i
|
|
if index < len(data) and data[index] == 0:
|
|
data[index] = stroke
|
|
|
|
nextAngle = Angle(lines = [l2])
|
|
if l2.ishorizontal:
|
|
nextAngle.horizontalDir = right if l2.findEnd else left
|
|
else:
|
|
nextAngle.verticalDir = top if l2.findEnd else bottom
|
|
return nextAngle
|
|
|
|
def _find_outline(self, data, width, height, stroke, first):
|
|
horizontalLines = []
|
|
verticalLines = []
|
|
size = len(data)
|
|
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
lastY = j - 1
|
|
if data[index] == stroke and j != (height - 1):
|
|
isCross = False
|
|
if (i != 0 and data[index - 1] == stroke) or (i != (width - 1) and data[index + 1] == stroke):
|
|
isCross = True
|
|
if startY < 0 and isCross:
|
|
startY = j
|
|
continue
|
|
if not isCross:
|
|
continue
|
|
lastY = j
|
|
|
|
if startY >= 0:
|
|
if j == (height - 1) and data[index] == stroke:
|
|
lastY = j
|
|
if lastY == startY:
|
|
startY = -1
|
|
continue
|
|
isCross = False
|
|
lastIndex = lastY * width + i
|
|
if data[lastIndex - 1] == stroke or data[lastIndex + 1] == stroke:
|
|
isCross = True
|
|
|
|
if isCross:
|
|
verticalLines.append(CLine(x = i, y = [startY, lastY], ishorizontal = False, length = (lastY - startY)))
|
|
startY = lastY
|
|
continue
|
|
startY = -1
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
lastX = i - 1
|
|
if data[index] == stroke and i != (width - 1):
|
|
isCross = False
|
|
if data[index - width] == stroke or data[index + width] == stroke:
|
|
isCross = True
|
|
if startX < 0 and isCross:
|
|
startX = i
|
|
continue
|
|
if not isCross:
|
|
continue
|
|
|
|
lastX = i
|
|
|
|
if startX >= 0:
|
|
if data[index] == stroke and i == (width - 1):
|
|
lastX = i
|
|
|
|
if lastX == startX:
|
|
startX = -1
|
|
continue
|
|
isCross = False
|
|
nIndex = lastIndex - width
|
|
nnIndex = lastIndex + width
|
|
if (nIndex >= 0 and data[nIndex] == stroke) or (nnIndex < size and data[nnIndex] == stroke):
|
|
isCross = True
|
|
|
|
if isCross:
|
|
horizontalLines.append(CLine(x = [startX, lastX], y = j, ishorizontal = True, length = (lastX - startX)))
|
|
startX = lastX
|
|
continue
|
|
startX = -1
|
|
|
|
if not horizontalLines:
|
|
return False
|
|
|
|
paths = self._find_bounds(
|
|
data, width, horizontalLines, verticalLines)
|
|
|
|
covertlines = None
|
|
allLines = None
|
|
totalLen = 0
|
|
tmp = []
|
|
for i in range(len(paths)):
|
|
item = paths[i]
|
|
plen = item.length
|
|
if plen > totalLen:
|
|
if covertlines and totalLen < 80:
|
|
tmp.append(covertlines)
|
|
totalLen = plen
|
|
covertlines = item.clines
|
|
allLines = item.alines
|
|
else:
|
|
if plen < 80:
|
|
tmp.append(item.clines)
|
|
|
|
if first and tmp:
|
|
clearPos = []
|
|
for i in range(len(tmp)):
|
|
clearPos.append([tmp[i][0].p0.x, tmp[i][0].p0.y])
|
|
|
|
while clearPos:
|
|
pos = clearPos.pop()
|
|
x = pos[0]
|
|
y = pos[1]
|
|
data[y * width + x] = 0
|
|
for _i in range(x - 1, x + 2):
|
|
for _j in range(y - 1, y + 2):
|
|
if _i == x or _j == y:
|
|
index = (_j * width) + _i
|
|
if index < len(data) and data[index] != 0:
|
|
clearPos.append([_i, _j])
|
|
|
|
bottom = 5
|
|
right = 6
|
|
top = 7
|
|
left = 8
|
|
dirnone = 0
|
|
|
|
angle = Angle()
|
|
for i in range(len(allLines) + 1):
|
|
line = allLines[0 if i == len(allLines) else i]
|
|
|
|
if i == 0:
|
|
angle.lines.append(line)
|
|
angle.horizontalDir = right
|
|
else:
|
|
if line.ishorizontal:
|
|
horizontalDir = right if line.findEnd else left
|
|
if angle.horizontalDir != dirnone and angle.horizontalDir != horizontalDir:
|
|
angle = self._fill_angle(
|
|
data, width, stroke, angle)
|
|
|
|
if angle.horizontalDir == dirnone:
|
|
angle.horizontalDir = horizontalDir
|
|
angle.lines.append(line)
|
|
else:
|
|
verticalDir = top if line.findEnd else bottom
|
|
if angle.verticalDir != dirnone and angle.verticalDir != verticalDir:
|
|
angle = self._fill_angle(
|
|
data, width, stroke, angle)
|
|
if angle.verticalDir == dirnone:
|
|
angle.verticalDir = verticalDir
|
|
angle.lines.append(line)
|
|
|
|
if line.length >= 7 or i == len(allLines):
|
|
angle = self._fill_angle(
|
|
data, width, stroke, angle)
|
|
|
|
return True
|
|
|
|
def _find_obstacle_border(self, data, width, height, stroke):
|
|
size = len(data)
|
|
for j in range(height):
|
|
for i in range(width):
|
|
index = j * width + i
|
|
if data[index] == stroke:
|
|
if j == 0 or j == (height - 1) or i == 0 or i == (width - 1):
|
|
data[index] = 2
|
|
continue
|
|
hasFind = False
|
|
for _i in range(i - 1, i + 2):
|
|
for _j in range(j - 1, j + 2):
|
|
nIndex = _j * width + _i
|
|
if nIndex < size and data[nIndex] != stroke and data[nIndex] != 2:
|
|
hasFind = True
|
|
break
|
|
if hasFind:
|
|
break
|
|
|
|
if hasFind:
|
|
data[index] = 2
|
|
|
|
def _clean_small_obstacle(self, data, width, height, stroke):
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
if data[index] == stroke:
|
|
if startY < 0:
|
|
startY = j
|
|
continue
|
|
if startY != -1 and (j - startY) <= 3:
|
|
for k in range(startY, j):
|
|
data[k * width + i] = 1
|
|
startY = -1
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
if data[index] == stroke:
|
|
if startX < 0:
|
|
startX = i
|
|
continue
|
|
if startX != -1 and (i - startX) <= 3:
|
|
for k in range(startX, i):
|
|
data[j * width + k] = 1
|
|
startX = -1
|
|
|
|
def _calculate_charger_position(self, data, width, height, stroke, charger_position):
|
|
vLines = []
|
|
hLines = []
|
|
for i in range(width):
|
|
startY = -1
|
|
for j in range(height):
|
|
index = j * width + i
|
|
lastY = j - 1
|
|
if data[index] == stroke and j != (height - 1):
|
|
isCross = False
|
|
if (i != 0 and data[index - 1] == stroke) or (i != width - 1 and data[index + 1] == stroke):
|
|
isCross = True
|
|
if startY < 0 and isCross:
|
|
startY = j
|
|
continue
|
|
if not isCross:
|
|
continue
|
|
lastY = j
|
|
if startY >= 0:
|
|
if j == height - 1 and data[index] == stroke:
|
|
lastY = j
|
|
if lastY == startY:
|
|
startY = -1
|
|
continue
|
|
isCross = False
|
|
lastIndex = lastY * width + i
|
|
if data[lastIndex - 1] == stroke or data[lastIndex + 1] == stroke:
|
|
isCross = True
|
|
|
|
if isCross:
|
|
vLines.append([[i, startY],[i, lastY]])
|
|
startY = lastY
|
|
continue
|
|
startY = -1
|
|
|
|
for j in range(height):
|
|
startX = -1
|
|
for i in range(width):
|
|
index = j * width + i
|
|
lastX = i - 1
|
|
if data[index] == stroke and i != width - 1:
|
|
isCross = False
|
|
if data[index - width] == stroke or data[index + width] == stroke:
|
|
isCross = True
|
|
if startX < 0 and isCross:
|
|
startX = i
|
|
continue
|
|
if not isCross:
|
|
continue
|
|
lastX = i
|
|
if startX >= 0:
|
|
if data[index] == stroke and i == width - 1:
|
|
lastX = i
|
|
|
|
if lastX == startX:
|
|
startX = -1
|
|
continue
|
|
isCross = False
|
|
lastIndex = j * width + lastX
|
|
if data[lastIndex - width] == stroke or data[lastIndex + width] == stroke:
|
|
isCross = True
|
|
|
|
if isCross:
|
|
hLines.append([[startX, j],[lastX, j]])
|
|
|
|
startX = lastX
|
|
continue
|
|
startX = -1
|
|
|
|
cX = math.floor(charger_position.x)
|
|
cY = math.floor(charger_position.y)
|
|
if abs(charger_position.a - 180) <= 30:
|
|
charger_position.a = 180
|
|
lastX = None
|
|
for i in range(len(vLines)):
|
|
line = vLines[i]
|
|
lx = line[0][0]
|
|
minY = line[0][1] if line[0][1] < line[1][1] else line[1][1]
|
|
maxY = line[0][1] if line[0][1] > line[1][1] else line[1][1]
|
|
if lx >= cX and cY >= minY and cY <= maxY:
|
|
if lastX == None or lx < lastX:
|
|
lastX = lx
|
|
if lastX != None:
|
|
if lastX - cX <= 11:
|
|
charger_position.a = 180
|
|
charger_position.x = lastX + 0.5
|
|
elif abs(charger_position.a - 360) <= 30 or abs(charger_position.a) <= 3:
|
|
charger_position.a = 360
|
|
lastX = None
|
|
for i in range(len(vLines)):
|
|
line = vLines[i]
|
|
lx = line[0][0]
|
|
minY = line[0][1] if line[0][1] < line[1][1] else line[1][1]
|
|
maxY = line[0][1] if line[0][1] > line[1][1] else line[1][1]
|
|
if lx <= cX and cY >= minY and cY <= maxY:
|
|
if lastX == None or lx > lastX:
|
|
lastX = lx
|
|
if lastX != None:
|
|
if cX - lastX <= 11:
|
|
charger_position.a = 360
|
|
charger_position.x = lastX + 0.5
|
|
elif abs(abs(charger_position.a - 270) <= 30):
|
|
lastY = None
|
|
for i in range(len(hLines)):
|
|
line = hLines[i]
|
|
ly = line[0][1]
|
|
minX = line[0][0] if line[0][0] < line[1][0] else line[1][0]
|
|
maxX = line[0][0] if line[0][0] > line[1][0] else line[1][0]
|
|
if ly >= cY and cX >= minX and cX <= maxX:
|
|
if lastY == None or ly < lastY:
|
|
lastY = ly
|
|
if lastY != None:
|
|
if lastY - cY <= 11:
|
|
charger_position.a = 270
|
|
charger_position.y = lastY + 0.5
|
|
elif abs(abs(charger_position.a - 90) <= 30):
|
|
lastY = None
|
|
for i in range(len(hLines)):
|
|
line = hLines[i]
|
|
ly = line[0][1]
|
|
minX = line[0][0] if line[0][0] < line[1][0] else line[1][0]
|
|
maxX = line[0][0] if line[0][0] > line[1][0] else line[1][0]
|
|
if ly <= cY and cX >= minX and cX <= maxX:
|
|
if lastY == None or ly > lastY:
|
|
lastY = ly
|
|
if lastY != None:
|
|
if cY - lastY <= 11:
|
|
charger_position.a = 90
|
|
charger_position.y = lastY + 0.5
|
|
|
|
return charger_position
|
|
|
|
def _merge_saved_map_data(self, map_data, saved_map_data, original_data = None):
|
|
if saved_map_data:
|
|
maxX = map_data.dimensions.left + \
|
|
(map_data.dimensions.width * map_data.dimensions.grid_size)
|
|
maxY = map_data.dimensions.top + \
|
|
(map_data.dimensions.height * map_data.dimensions.grid_size)
|
|
|
|
if maxX < saved_map_data.dimensions.left + (saved_map_data.dimensions.width * saved_map_data.dimensions.grid_size):
|
|
maxX = saved_map_data.dimensions.left + \
|
|
(saved_map_data.dimensions.width *
|
|
saved_map_data.dimensions.grid_size)
|
|
|
|
if maxY < saved_map_data.dimensions.top + (saved_map_data.dimensions.height * saved_map_data.dimensions.grid_size):
|
|
maxY = saved_map_data.dimensions.top + \
|
|
(saved_map_data.dimensions.height *
|
|
saved_map_data.dimensions.grid_size)
|
|
|
|
left = map_data.dimensions.left
|
|
top = map_data.dimensions.top
|
|
|
|
if saved_map_data.dimensions.left < left:
|
|
left = saved_map_data.dimensions.left
|
|
|
|
if saved_map_data.dimensions.top < top:
|
|
top = saved_map_data.dimensions.top
|
|
|
|
width = int((maxX - left) / saved_map_data.dimensions.grid_size)
|
|
height = int((maxY - top) / saved_map_data.dimensions.grid_size)
|
|
|
|
si = int((saved_map_data.dimensions.left - left) /
|
|
saved_map_data.dimensions.grid_size)
|
|
sj = int((saved_map_data.dimensions.top - top) /
|
|
saved_map_data.dimensions.grid_size)
|
|
|
|
sim = (si + saved_map_data.dimensions.width)
|
|
sjm = (sj + saved_map_data.dimensions.height)
|
|
|
|
ni = int((map_data.dimensions.left - left) /
|
|
map_data.dimensions.grid_size)
|
|
nj = int((map_data.dimensions.top - top) /
|
|
map_data.dimensions.grid_size)
|
|
|
|
nim = ni + map_data.dimensions.width
|
|
njm = nj + map_data.dimensions.height
|
|
|
|
pixel_type = np.zeros((width, height), np.uint8)
|
|
data = map_data.optimized_pixel_type if map_data.optimized_pixel_type is not None else map_data.pixel_type
|
|
|
|
for j in range(height):
|
|
for i in range(width):
|
|
if j >= sj and i >= si and j < sjm and i < sim:
|
|
saved_value = int(saved_map_data.pixel_type[(i - si), (j - sj)])
|
|
else:
|
|
saved_value = 0
|
|
|
|
if j >= nj and i >= ni and j < njm and i < nim:
|
|
clean_value = int(data[(i - ni), (j - nj)])
|
|
else:
|
|
clean_value = 0
|
|
|
|
if saved_value != 0:
|
|
if saved_value != 255:
|
|
pixel_type[i, j] = saved_value
|
|
else:
|
|
if clean_value != 0 and clean_value != 255:
|
|
pixel_type[i, j] = 254
|
|
else:
|
|
pixel_type[i, j] = 255
|
|
elif clean_value != 0:
|
|
if clean_value == 255:
|
|
pixel_type[i, j] = 255
|
|
else:
|
|
pixel_type[i, j] = 254
|
|
|
|
if original_data is not None:
|
|
for j in range(height):
|
|
for i in range(width):
|
|
if j >= nj and i >= ni and j < njm and i < nim:
|
|
if original_data[(j - nj) * map_data.dimensions.width + (i - ni)] == 2 and pixel_type[i, j] != 0:
|
|
dis = 3
|
|
hasBorder = False
|
|
for _j in range(j - dis, j + dis + 1):
|
|
for _i in range(i - dis, i + dis):
|
|
if _j < 0 or _i < 0 or _j >= height or _i >= width:
|
|
continue
|
|
if hasBorder:
|
|
break
|
|
if pixel_type[_i, _j] == 255:
|
|
hasBorder = True
|
|
break
|
|
|
|
if not hasBorder:
|
|
pixel_type[i, j] = 251
|
|
|
|
map_data.optimized_pixel_type = pixel_type
|
|
map_data.optimized_dimensions = MapImageDimensions(top, left, height, width, map_data.dimensions.grid_size)
|
|
|
|
def optimize(self, map_data, saved_map_data = None, js_optimizer = True):
|
|
if map_data.saved_map:
|
|
return map_data
|
|
|
|
try:
|
|
now = time.time()
|
|
|
|
if js_optimizer:
|
|
if self._js_optimizer == None:
|
|
self._js_optimizer = MiniRacer()
|
|
self._js_optimizer.eval(base64.b64decode(MAP_OPTIMIZER_JS))
|
|
|
|
data = map_data.pixel_type.tolist()
|
|
data_size = [map_data.dimensions.left, map_data.dimensions.top, map_data.dimensions.width, map_data.dimensions.height, map_data.dimensions.grid_size]
|
|
saved_data = saved_map_data.pixel_type.tolist() if saved_map_data else None
|
|
saved_data_size = [saved_map_data.dimensions.left, saved_map_data.dimensions.top, saved_map_data.dimensions.width, saved_map_data.dimensions.height, saved_map_data.dimensions.grid_size] if saved_map_data else None
|
|
charger_position = None
|
|
if map_data.charger_position:
|
|
left = map_data.dimensions.left
|
|
top = map_data.dimensions.top
|
|
|
|
if saved_map_data:
|
|
if saved_map_data.dimensions.left < left:
|
|
left = saved_map_data.dimensions.left
|
|
|
|
if saved_map_data.dimensions.top < top:
|
|
top = saved_map_data.dimensions.top
|
|
|
|
charger_position = [(map_data.charger_position.x - left) / map_data.dimensions.grid_size, (map_data.charger_position.y - top) / map_data.dimensions.grid_size, map_data.charger_position.a]
|
|
|
|
result = self._js_optimizer.call('optimize', data, data_size, saved_data, saved_data_size, charger_position)
|
|
if result and result[0]:
|
|
map_data.optimized_pixel_type = np.array(result[0], dtype=np.uint8)
|
|
|
|
dimensions = result[1]
|
|
map_data.optimized_dimensions = MapImageDimensions(dimensions[1], dimensions[0], dimensions[3], dimensions[2], map_data.dimensions.grid_size)
|
|
|
|
if result[2] and map_data.charger_position:
|
|
charger = result[2]
|
|
#map_data.optimized_charger_position = Point(charger[0] * map_data.dimensions.grid_size + left, charger[1] * map_data.dimensions.grid_size + top, charger[2])
|
|
else:
|
|
width = map_data.dimensions.width
|
|
height = map_data.dimensions.height
|
|
clean_data = np.zeros((width * height), np.uint8).tolist()
|
|
|
|
data_map = {255:2, 253:1, 250:3}
|
|
pointNum = 0
|
|
for j in range(height):
|
|
for i in range(width):
|
|
index = j * width + i
|
|
clean_data[index] = int(map_data.pixel_type[i, j])
|
|
if clean_data[index]:
|
|
pointNum = pointNum + 1
|
|
clean_data[index] = data_map.get(clean_data[index], 0)
|
|
|
|
original_data = clean_data.copy()
|
|
pixel_type = np.zeros((width, height), np.uint8)
|
|
|
|
self._clean_wall(clean_data, width, height)
|
|
self._fill_map_data(clean_data, width, height, 3)
|
|
self._denoise(clean_data, width, height)
|
|
self._update_border_value(clean_data, width, height, 5)
|
|
self._fill_cross_line(clean_data, width, height, 5)
|
|
self._link_adjacent_areas(original_data, clean_data, width, height, 5)
|
|
|
|
result = self._find_outline(clean_data, width, height, 5, True)
|
|
if result:
|
|
self._fill_map_data_2(clean_data, width, height)
|
|
self._update_border_value(clean_data, width, height, 6)
|
|
if map_data.charger_position:
|
|
left = map_data.dimensions.left
|
|
top = map_data.dimensions.top
|
|
|
|
if saved_map_data:
|
|
if saved_map_data.dimensions.left < left:
|
|
left = saved_map_data.dimensions.left
|
|
|
|
if saved_map_data.dimensions.top < top:
|
|
top = saved_map_data.dimensions.top
|
|
|
|
new_charger_position = copy.deepcopy(map_data.charger_position)
|
|
new_charger_position.x = int((new_charger_position.x - left) / map_data.dimensions.grid_size)
|
|
new_charger_position.y = int((new_charger_position.y - top) / map_data.dimensions.grid_size)
|
|
if new_charger_position.y >= 0 and new_charger_position.x >= 0 and new_charger_position.y < height and new_charger_position.x < width and clean_data[int(math.floor(new_charger_position.y)) * width + int(math.floor(new_charger_position.x))]:
|
|
new_charger_position = self._calculate_charger_position(clean_data, width, height, 6, new_charger_position)
|
|
map_data.optimized_charger_position = Point(int(new_charger_position.x * map_data.dimensions.grid_size) + left, int(new_charger_position.y * map_data.dimensions.grid_size) + top, new_charger_position.a)
|
|
|
|
self._find_outline(clean_data, width, height, 6, False)
|
|
self._fill_map_data_2(clean_data, width, height)
|
|
self._update_border_value(clean_data, width, height, 7)
|
|
|
|
if saved_map_data:
|
|
self._find_obstacle_border(clean_data, width, height, 3)
|
|
self._obstacle_data(original_data, width, height)
|
|
else:
|
|
self._clean_small_obstacle(clean_data, width, height, 3)
|
|
|
|
currentPointNum = 0
|
|
data_map = {7:255, 2:255, 3:(0 if saved_map_data else 250)}
|
|
for j in range(height):
|
|
for i in range(width):
|
|
clean_value = clean_data[j * width + i]
|
|
if clean_value != 0:
|
|
currentPointNum = currentPointNum + 1
|
|
pixel_type[i, j] = data_map.get(clean_value, 253)
|
|
|
|
if (not ((currentPointNum * 100) / pointNum) < 50 and pointNum > 2000):
|
|
map_data.optimized_pixel_type = pixel_type
|
|
|
|
self._merge_saved_map_data(map_data, saved_map_data, original_data)
|
|
|
|
_LOGGER.info(
|
|
"Optimize Map Data: %s:%s took: %.2f",
|
|
map_data.map_id,
|
|
map_data.frame_id,
|
|
time.time() - now,
|
|
)
|
|
except Exception as ex:
|
|
_LOGGER.warning("Optimize map failed: %s", ex)
|
|
|
|
self._merge_saved_map_data(map_data, saved_map_data)
|
|
|
|
#_LOGGER.warn(f"""
|
|
#var data = {map_data.pixel_type.tolist()};
|
|
#var data_size = {[map_data.dimensions.left, map_data.dimensions.top, map_data.dimensions.width, map_data.dimensions.height, map_data.dimensions.grid_size]};
|
|
#var saved_data = {saved_map_data.pixel_type.tolist() if saved_map_data else "undefined"};
|
|
#var saved_data_size = {[saved_map_data.dimensions.left, saved_map_data.dimensions.top, saved_map_data.dimensions.width, saved_map_data.dimensions.height, saved_map_data.dimensions.grid_size] if saved_map_data else "undefined"};
|
|
#var charger_position = {[map_data.charger_position.x, map_data.charger_position.y, map_data.charger_position.a] if map_data.charger_position else "undefined"};
|
|
# """)
|
|
|
|
return map_data
|