| | |
| | | |
| | | import cn.hutool.http.HttpUtil; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.sandu.common.domain.CommonPage; |
| | | import com.sandu.common.execption.BusinessException; |
| | | import com.sandu.common.object.BaseConditionVO; |
| | | import com.sandu.common.service.impl.BaseServiceImpl; |
| | | import com.sandu.common.util.SpringContextHolder; |
| | | import com.sandu.ximon.admin.dto.YSY_AccessTokenDto; |
| | | import com.sandu.ximon.admin.dto.YSY_MonitorDto; |
| | | import com.sandu.ximon.admin.param.MonitorParam; |
| | | import com.sandu.ximon.admin.redis.DeviceRedisKey; |
| | | import com.sandu.ximon.admin.security.SecurityUtils; |
| | | import com.sandu.ximon.admin.utils.ListPagingUtils; |
| | | import com.sandu.ximon.admin.utils.RedisUtils; |
| | | import com.sandu.ximon.admin.utils.StoreOperationRecordsUtils; |
| | | import com.sandu.ximon.admin.vo.EquipmentInfomation; |
| | | import com.sandu.ximon.admin.vo.RedisDeviceStatus; |
| | | import com.sandu.ximon.dao.bo.MonitorBo; |
| | | import com.sandu.ximon.dao.domain.Monitor; |
| | | import com.sandu.ximon.dao.domain.YSY_AccessTokenDto; |
| | | import com.sandu.ximon.dao.domain.YSY_MonitorDto; |
| | | import com.sandu.ximon.dao.domain.PoleBinding; |
| | | import com.sandu.ximon.dao.enums.OrderByEnums; |
| | | import com.sandu.ximon.dao.enums.PoleBindingEnums; |
| | | import com.sandu.ximon.dao.mapper.MonitorMapper; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.CountDownLatch; |
| | | |
| | | @Service |
| | | @AllArgsConstructor |
| | | @Slf4j |
| | | public class MonitorService extends BaseServiceImpl<MonitorMapper, Monitor> { |
| | | private final MonitorMapper monitorMapper; |
| | | //萤石云AppKey |
| | |
| | | private static final String DELETE_DEVICE_URL = "/device/delete"; |
| | | //获取萤石云单个设备信息 |
| | | private static final String GET_DEVICE_URL = "/device/info"; |
| | | //获取萤石云摄像头播放地址URL |
| | | private static final String GET_VEDIO_URL = "/v2/live/address/get"; |
| | | |
| | | |
| | | private final PoleBindingService poleBindingService; |
| | | |
| | | private final RedisUtils redisUtils; |
| | | |
| | | /** |
| | | * 查询全部摄像头 |
| | | * 摄像头列表模糊查询 |
| | | */ |
| | | public List<Monitor> listMonitor() { |
| | | LambdaQueryWrapper<Monitor> wrapper = Wrappers.lambdaQuery(Monitor.class); |
| | | List<Monitor> list = list(wrapper); |
| | | public CommonPage listMonitorByKeyword(BaseConditionVO baseConditionVO, Integer order, Integer seq, MonitorParam monitorParam) { |
| | | //绑定状态:已绑定/未绑定/全部 |
| | | if (monitorParam.getBindingState() == null) { |
| | | //全部 |
| | | monitorParam.setBindingState(2); |
| | | } |
| | | //排序字段 |
| | | String orderByResult = "id"; |
| | | //正序、倒叙 |
| | | String orderBySeq = OrderByEnums.ASC.getCode(); |
| | | if (order != null) { |
| | | switch (order) { |
| | | case 1: |
| | | orderByResult = OrderByEnums.MONITOR_POLE_NAME.getCode(); |
| | | break; |
| | | case 2: |
| | | orderByResult = OrderByEnums.MONITOR_CREATE_TIME.getCode(); |
| | | break; |
| | | default: |
| | | } |
| | | } |
| | | if (seq != null) { |
| | | switch (seq) { |
| | | case 1: |
| | | orderBySeq = " ASC"; |
| | | break; |
| | | case 2: |
| | | orderBySeq = " DESC"; |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | //排序方式 |
| | | String orderBy = orderByResult + " " + orderBySeq; |
| | | |
| | | List<MonitorBo> list; |
| | | if (SecurityUtils.getClientId() == null) { |
| | | list = monitorMapper.listMonitorDeviceSerial1(null, monitorParam.getKeyword(), monitorParam.getBindingState(), orderBy); |
| | | } else { |
| | | list = monitorMapper.listMonitorDeviceSerial1(SecurityUtils.getUserId(), monitorParam.getKeyword(), monitorParam.getBindingState(), orderBy); |
| | | } |
| | | |
| | | //没有数据时候直接返回空数组 |
| | | if (list.size() == 0 || list == null) { |
| | | return new CommonPage(); |
| | | } |
| | | List<String> device_serialList = new ArrayList<>(list.size()); |
| | | CountDownLatch latch = new CountDownLatch(list.size()); |
| | | for (Monitor demo : list) { |
| | | device_serialList.add(demo.getDeviceSerial()); |
| | | getMonitorInfo_Async(demo, latch); |
| | | } |
| | | try { |
| | | latch.await(); |
| | | } catch (InterruptedException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | if (monitorParam.getEquipmentState() == null) { |
| | | //全部 |
| | | monitorParam.setEquipmentState(2); |
| | | } |
| | | |
| | | List<MonitorBo> temp = new ArrayList<>(); |
| | | if (monitorParam.getEquipmentState() == 2) { |
| | | temp.addAll(list); |
| | | } else { |
| | | for (MonitorBo monitorBo : list) { |
| | | //筛选状态数据 |
| | | if (monitorBo.getEquipmentState().equals(monitorParam.getEquipmentState())) { |
| | | temp.add(monitorBo); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (baseConditionVO == null) { |
| | | baseConditionVO = new BaseConditionVO(); |
| | | baseConditionVO.setPageSize(null); |
| | | baseConditionVO.setPageNo(null); |
| | | } |
| | | CommonPage commonPage = ListPagingUtils.pages(temp, baseConditionVO.getPageNo(), baseConditionVO.getPageSize()); |
| | | return commonPage; |
| | | } |
| | | |
| | | /** |
| | | * 摄像头列表地(用于首页数据统计) |
| | | * |
| | | * @return |
| | | */ |
| | | public List<MonitorBo> listMonitorOnHome() { |
| | | |
| | | List<MonitorBo> list; |
| | | if (SecurityUtils.getClientId() == null) { |
| | | list = monitorMapper.listMonitorDeviceSerial1(null, null, 2, null); |
| | | } else { |
| | | list = monitorMapper.listMonitorDeviceSerial1(SecurityUtils.getUserId(), null, 2, null); |
| | | } |
| | | |
| | | //没有数据时候直接返回空数组 |
| | | if (list == null) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | | * 模糊查询 |
| | | */ |
| | | public List<Monitor> listMonitorByKeyword(MonitorParam monitorParam) { |
| | | //设备状态:在线/离线/全部 |
| | | int equipmentState1; |
| | | int equipmentState2; |
| | | if (2 == monitorParam.getEquipmentState()) { |
| | | //在线 |
| | | equipmentState1 = 1; |
| | | //离线 |
| | | equipmentState2 = 0; |
| | | } else { |
| | | equipmentState1 = monitorParam.getEquipmentState(); |
| | | equipmentState2 = monitorParam.getEquipmentState(); |
| | | } |
| | | |
| | | //绑定状态:已绑定/未绑定/全部 |
| | | int bindingState1; |
| | | int bindingState2; |
| | | if (2 == monitorParam.getBindingState()) { |
| | | //已绑定 |
| | | bindingState1 = 1; |
| | | //未绑定 |
| | | bindingState2 = 0; |
| | | } else { |
| | | bindingState1 = monitorParam.getBindingState(); |
| | | bindingState2 = monitorParam.getBindingState(); |
| | | } |
| | | return monitorMapper.listMonitorByKeyword(monitorParam.getKeyword(), equipmentState1, equipmentState2, bindingState1, bindingState2); |
| | | } |
| | | |
| | | /** |
| | | * 删除摄像头 |
| | | * |
| | | * @param Id |
| | | * @param |
| | | * @return |
| | | */ |
| | | public boolean deleteMonitor(Long Id) { |
| | | Monitor monitor = getById(Id); |
| | | if (monitor == null) { |
| | | throw new BusinessException("找不到摄像头"); |
| | | public boolean deleteMonitor(List<Long> ids) { |
| | | List<Long> idList = new ArrayList<Long>(); |
| | | List<Monitor> monitors = listByIds(ids); |
| | | if (!monitors.isEmpty()) { |
| | | monitors.forEach( |
| | | monitor -> { |
| | | if (monitor == null) { |
| | | throw new BusinessException("找不到摄像头"); |
| | | } |
| | | PoleBinding one = poleBindingService.getOne(Wrappers.lambdaQuery(PoleBinding.class).eq(PoleBinding::getDeviceCode, monitor.getDeviceSerial())); |
| | | if (one != null) { |
| | | //删除摄像头设备前需要先解绑 |
| | | poleBindingService.unBindPole(null, monitor.getDeviceSerial()); |
| | | } |
| | | /** |
| | | * 删除摄像头日志记录开始 |
| | | */ |
| | | String content = "{摄像头Code:" + monitor.getDeviceSerial() + ", 摄像头名称:" + monitor.getDeviceName() + " }"; |
| | | List<String> listCode = new ArrayList<>(); |
| | | listCode.add(monitor.getDeviceSerial()); |
| | | StoreOperationRecordsUtils.storeOperationData(listCode, null, "删除摄像头", content); |
| | | /** |
| | | * 删除摄像头日志记录结束 |
| | | */ |
| | | idList.add(monitor.getId()); |
| | | } |
| | | ); |
| | | } |
| | | return removeById(Id); |
| | | //全部删除成功 |
| | | return removeByIds(idList); |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | paramMap.put("appKey", AppKey); |
| | | paramMap.put("appSecret", Secret); |
| | | String s = HttpUtil.post(YSY_URL + ACCESSTOKEN_URL, paramMap); |
| | | if (!s.startsWith("{") || !s.endsWith("}")) { |
| | | log.error("获取摄像头token失败,格式有误!"); |
| | | } |
| | | YSY_AccessTokenDto accessTokenDto = JSON.parseObject(s, YSY_AccessTokenDto.class); |
| | | |
| | | //二次获取 |
| | | if (accessTokenDto == null) { |
| | | log.error("获取摄像头token失败,格式有误!"); |
| | | s = HttpUtil.post(YSY_URL + ACCESSTOKEN_URL, paramMap); |
| | | accessTokenDto = JSON.parseObject(s, YSY_AccessTokenDto.class); |
| | | } |
| | | return accessTokenDto.getData().getAccessToken(); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 获取萤石云摄像头信息(异步) |
| | | * |
| | | * @return |
| | | */ |
| | | public void getMonitorInfo_Async(Monitor monitor, CountDownLatch countDownLatch) { |
| | | Thread t = new Thread() { |
| | | @Override |
| | | public void run() { |
| | | String token = getAccessToken(); |
| | | Map<String, Object> paramMap = new HashMap<>(); |
| | | paramMap.put("accessToken", token); |
| | | paramMap.put("deviceSerial", monitor.getDeviceSerial()); |
| | | String s = HttpUtil.post(YSY_URL + GET_DEVICE_URL, paramMap); |
| | | YSY_MonitorDto monitorDto = null; |
| | | try { |
| | | monitorDto = JSON.parseObject(s, YSY_MonitorDto.class); |
| | | System.out.println(s + " ----------"); |
| | | monitor.setEquipmentState(monitorDto.getData().getStatus()); |
| | | countDownLatch.countDown(); |
| | | } catch (Exception e) { |
| | | monitor.setEquipmentState(0); |
| | | countDownLatch.countDown(); |
| | | } |
| | | } |
| | | }; |
| | | t.start(); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 获取萤石云摄像头信息 无需验证accessToken |
| | | * |
| | | * @return |
| | | */ |
| | | public EquipmentInfomation getMonitorInfo(String deviceSerial) { |
| | | EquipmentInfomation equipmentInfo = new EquipmentInfomation(); |
| | | equipmentInfo.setEquipmentType("摄像头"); |
| | | if (deviceSerial == null || deviceSerial.trim().length() == 0) { |
| | | return equipmentInfo; |
| | | } |
| | | |
| | | Monitor one = getOne(Wrappers.lambdaQuery(Monitor.class).eq(Monitor::getDeviceSerial, deviceSerial)); |
| | | if (one != null) { |
| | | equipmentInfo.setEquipmentCreateTime(one.getAddTime()); |
| | | equipmentInfo.setEquipmentMac(one.getDeviceSerial()); |
| | | equipmentInfo.setEquipmentName(one.getDeviceName()); |
| | | } |
| | | |
| | | String token = getAccessToken(); |
| | | Map<String, Object> paramMap = new HashMap<>(); |
| | | paramMap.put("accessToken", token); |
| | | paramMap.put("deviceSerial", deviceSerial); |
| | | String s = HttpUtil.post(YSY_URL + GET_DEVICE_URL, paramMap); |
| | | |
| | | try { |
| | | YSY_MonitorDto monitorDto = JSON.parseObject(s, YSY_MonitorDto.class); |
| | | if (monitorDto != null) { |
| | | //在线状态:0-不在线,1-在线 |
| | | if (monitorDto.getData().getStatus() == 1) { |
| | | equipmentInfo.setEquipmentState("在线"); |
| | | } else { |
| | | equipmentInfo.setEquipmentState("离线"); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return equipmentInfo; |
| | | } |
| | | |
| | | /** |
| | | * 添加萤石云摄像头 |
| | | * |
| | | * @param accessToken 授权过程获取的access_token |
| | | * @param deviceSerial 设备序列号,存在英文字母的设备序列号,字母需为大写 |
| | | * @param validateCode 设备验证码,设备机身上的六位大写字母 |
| | | * // * @param validateCode 设备验证码,设备机身上的六位大写字母 |
| | | */ |
| | | public YSY_MonitorDto addYSYMonitor(String accessToken, String deviceSerial, String validateCode) { |
| | | public boolean addYSYMonitor(String deviceSerial, String validateCode) { |
| | | List<Monitor> list = list(Wrappers.lambdaQuery(Monitor.class).eq(Monitor::getDeviceSerial, deviceSerial)); |
| | | if (list.size() != 0) { |
| | | throw new BusinessException("该摄像头已存在,无法重复添加!"); |
| | | } |
| | | |
| | | Map<String, Object> paramMap = new HashMap<>(); |
| | | paramMap.put("accessToken", accessToken); |
| | | paramMap.put("accessToken", getAccessToken()); |
| | | paramMap.put("deviceSerial", deviceSerial); |
| | | paramMap.put("validateCode", validateCode); |
| | | if (validateCode != null && validateCode.trim().length() != 0) { |
| | | paramMap.put("validateCode", validateCode); |
| | | } |
| | | String s = HttpUtil.post(YSY_URL + ADD_DEVICE_URL, paramMap); |
| | | System.out.println(s); |
| | | if (!s.startsWith("{") && !s.endsWith("}")) { |
| | | throw new BusinessException("萤石云数据异常"); |
| | | } |
| | | |
| | | YSY_MonitorDto monitorDto = JSON.parseObject(s, YSY_MonitorDto.class); |
| | | return monitorDto; |
| | | } |
| | | |
| | | boolean result; |
| | | if ("20017".equals(monitorDto.getCode())) { |
| | | //当前摄像头已经被萤石云官网添加过,Code:20017 |
| | | monitorDto = getYSYMonitorInfo(getAccessToken(), deviceSerial); |
| | | |
| | | /** |
| | | * 删除萤石云摄像头 |
| | | * |
| | | * @param accessToken 授权过程获取的access_token |
| | | * @param deviceSerial 设备序列号,存在英文字母的设备序列号,字母需为大写 |
| | | */ |
| | | public YSY_MonitorDto deleteYSYMonitor(String accessToken, String deviceSerial) { |
| | | } else if (!"200".equals(monitorDto.getCode())) { |
| | | //当前摄像头成功被萤石云官网添加,Code:200 |
| | | throw new BusinessException(monitorDto.getMsg()); |
| | | } |
| | | |
| | | Map<String, Object> paramMap = new HashMap<>(); |
| | | paramMap.put("accessToken", accessToken); |
| | | paramMap.put("deviceSerial", deviceSerial); |
| | | String s = HttpUtil.post(YSY_URL + DELETE_DEVICE_URL, paramMap); |
| | | System.out.println(s); |
| | | YSY_MonitorDto monitorDto = JSON.parseObject(s, YSY_MonitorDto.class); |
| | | return monitorDto; |
| | | if (monitorDto.getData() == null) { |
| | | throw new BusinessException("萤石云数据异常!"); |
| | | } |
| | | Monitor monitor = new Monitor(); |
| | | monitor.setDeviceSerial(monitorDto.getData().getDeviceSerial()); |
| | | monitor.setDevicesCode(validateCode); |
| | | monitor.setDeviceName(monitorDto.getData().getDeviceName()); |
| | | monitor.setEquipmentState(monitorDto.getData().getStatus()); |
| | | monitor.setIsEncrypt(monitorDto.getData().getIsEncrypt()); |
| | | monitor.setDeviceType(monitorDto.getData().getModel()); |
| | | monitor.setNetAddress(monitorDto.getData().getNetAddress()); |
| | | result = addMonitorInfoToTable(monitor); |
| | | |
| | | /** |
| | | * 新增摄像头开始 |
| | | */ |
| | | String content = "{摄像头Code:" + monitor.getDeviceSerial() + ", 摄像头名称:" + monitor.getDeviceName() + " }"; |
| | | List<String> listCode = new ArrayList<>(); |
| | | listCode.add(monitor.getDeviceSerial()); |
| | | StoreOperationRecordsUtils.storeOperationData(listCode, null, "新增摄像头", content); |
| | | /** |
| | | * 新增摄像头结束 |
| | | */ |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | |
| | | if (one == null) { |
| | | return save(monitor); |
| | | } else { |
| | | monitor.setId(one.getId()); |
| | | return updateById(monitor); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将摄像头设备相关信息更新到数据库 |
| | | * 返回摄像头实时画面播放地址 |
| | | * |
| | | * @param monitor 摄像头对象 |
| | | * @param accessToken |
| | | * @param deviceSerial |
| | | * @return |
| | | */ |
| | | public boolean upDateMonitorInfo(Monitor monitor) { |
| | | if (monitor.getId() == null) { |
| | | throw new BusinessException("缺少摄像头相关ID参数"); |
| | | public String getMonitorVideoUrl(String accessToken, String deviceSerial, int protocol, int expireTime) { |
| | | if (expireTime < 30 || expireTime > 62208000) { |
| | | throw new BusinessException("设置的URL播放地址过期时长数据错误"); |
| | | } |
| | | |
| | | Monitor info = getById(monitor.getId()); |
| | | |
| | | if (info == null) { |
| | | throw new BusinessException("找不到相关摄像头"); |
| | | Map<String, Object> paramMap = new HashMap<>(); |
| | | paramMap.put("accessToken", accessToken); |
| | | paramMap.put("deviceSerial", deviceSerial); |
| | | paramMap.put("protocol", protocol); |
| | | paramMap.put("expireTime", expireTime); |
| | | String s = HttpUtil.post(YSY_URL + GET_VEDIO_URL, paramMap); |
| | | System.out.println(s); |
| | | YSY_MonitorDto monitor = JSON.parseObject(s, YSY_MonitorDto.class); |
| | | if ("200".equals(monitor.getCode())) { |
| | | return monitor.getData().getUrl(); |
| | | } else { |
| | | throw new BusinessException(monitor.getMsg()); |
| | | } |
| | | if (!monitor.getDevicesCode().equals(info.getDevicesCode()) || !monitor.getDeviceSerial().equals(info.getDeviceSerial())) { |
| | | throw new BusinessException("监控序列号或监控验证码不正确"); |
| | | } |
| | | } |
| | | |
| | | return updateById(monitor); |
| | | /** |
| | | * 修改设备绑定状态 |
| | | * |
| | | * @param flag true为绑定 false 为解绑 |
| | | */ |
| | | public void updateBingdingState(boolean flag, String deviceCode) { |
| | | if (flag) {//绑定 |
| | | Monitor monitor = getOne(Wrappers.lambdaQuery(Monitor.class).eq(Monitor::getDeviceSerial, deviceCode)); |
| | | if (monitor == null) { |
| | | return; |
| | | } |
| | | monitor.setBindingState(1); |
| | | updateById(monitor); |
| | | } else {//解绑 |
| | | Monitor monitor = getOne(Wrappers.lambdaQuery(Monitor.class).eq(Monitor::getDeviceSerial, deviceCode)); |
| | | if (monitor == null) { |
| | | return; |
| | | } |
| | | monitor.setBindingState(0); |
| | | updateById(monitor); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 修改note |
| | | * |
| | | * @param deviceSerial |
| | | * @param notes |
| | | * @return |
| | | */ |
| | | public boolean updateNotes(String deviceSerial, String notes) { |
| | | if (deviceSerial == null || deviceSerial.trim().length() == 0 || notes == null) { |
| | | throw new BusinessException("修改备注参数有误!"); |
| | | } |
| | | Monitor one = getOne(Wrappers.lambdaQuery(Monitor.class).eq(Monitor::getDeviceSerial, deviceSerial)); |
| | | |
| | | boolean belong = SpringContextHolder.getBean(PoleBindingService.class).isBelong(one.getDeviceSerial(), PoleBindingEnums.MONITOR); |
| | | if (!belong) { |
| | | throw new BusinessException("该设备不属于您,不能修改设备信息"); |
| | | } |
| | | if (one != null) { |
| | | one.setNote(notes); |
| | | |
| | | /** |
| | | * 添加摄像头备注日志记录开始 |
| | | */ |
| | | String content = "编辑摄像头:" + deviceSerial + "编辑内容:" + notes; |
| | | List<String> list = new ArrayList<>(); |
| | | list.add(deviceSerial); |
| | | StoreOperationRecordsUtils.storeOperationData(list, null, "编辑摄像头", content); |
| | | /** |
| | | * 添加摄像头备注日志记录结束 |
| | | */ |
| | | return updateById(one); |
| | | } else { |
| | | throw new BusinessException("摄像头设备不存在!"); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 向Redis中存入设备状态 |
| | | */ |
| | | public void setCacheData() { |
| | | list().forEach( |
| | | device -> { |
| | | RedisDeviceStatus deviceStatus = new RedisDeviceStatus(); |
| | | deviceStatus.setDeviceId(device.getDeviceSerial()); |
| | | if (device != null) { |
| | | EquipmentInfomation monitorInfo = getMonitorInfo(device.getDeviceSerial()); |
| | | if (monitorInfo != null) { |
| | | if ("在线".equals(monitorInfo.getEquipmentState())) { |
| | | deviceStatus.setStatus(0); |
| | | } else { |
| | | deviceStatus.setStatus(1); |
| | | } |
| | | |
| | | } else { |
| | | deviceStatus.setStatus(1); |
| | | } |
| | | } |
| | | redisUtils.set(DeviceRedisKey.MONITOR + device.getDeviceSerial(), JSON.toJSONString(deviceStatus)); |
| | | // redisUtils.delete(DeviceRedisKey.MONITOR + device.getDeviceSerial()); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | |