From c2395016f935a00727af661ceb4f4dc456b81fd2 Mon Sep 17 00:00:00 2001 From: Victor Shcherb Date: Thu, 3 Sep 2020 12:39:18 +0200 Subject: [PATCH] Squashed commit of the following: commit 9a8a4e16b282ffd4ddf02b364a362b9d57440b74 Author: simon Date: Thu Sep 3 00:13:28 2020 +0300 server refactoring1 commit e47493a22973fd65ab73901e97da025546e2a6f0 Author: simon Date: Wed Sep 2 23:31:12 2020 +0300 server refactoring commit 6a9563007677704c97912180084426cb4b855f89 Author: simon Date: Wed Sep 2 18:40:42 2020 +0300 api router mapactivity changes commit e6a345e0e94581c8d9bc4042294ecbb583e1c0fc Author: simon Date: Mon Aug 31 18:10:11 2020 +0300 add variables lat lon zoom commit 04bb04f21c6510da28135294cb34ec8504139be7 Author: simon Date: Mon Aug 31 16:45:54 2020 +0300 tiles by lat lon commit 3019e35d041cfc7bbc10b70a11d563901d416a13 Author: simon Date: Mon Aug 31 15:45:53 2020 +0300 changing resourceManager commit 8a8fcd8d58c260af50bfd3540d26e6d2c808a717 Author: simon Date: Mon Aug 31 15:26:28 2020 +0300 cleanup commit 3f9b0f03d436472112ebcd38cea01e47deb06ced Author: simon Date: Mon Aug 31 11:35:00 2020 +0300 display loaded tile commit db49b925ca517b980c9ee4f831709d5fb1bc59b1 Author: simon Date: Thu Aug 27 14:29:57 2020 +0300 saving tile response commit ef2bc3677c9c198a4894c9f5e0e99041b845d8f0 Author: simon Date: Tue Aug 25 13:17:33 2020 +0300 markers coloured --- .../java/net/osmand/data/RotatedTileBox.java | 53 + .../server/css/leaflet.awesome-markers.css | 57 + OsmAnd/assets/server/go.html | 101 ++ OsmAnd/assets/server/images/markers-matte.png | Bin 0 -> 14323 bytes .../assets/server/images/markers-matte@2x.png | Bin 0 -> 31113 bytes OsmAnd/assets/server/images/markers-plain.png | Bin 0 -> 7946 bytes .../assets/server/images/markers-shadow.png | Bin 0 -> 535 bytes .../server/images/markers-shadow@2x.png | Bin 0 -> 1469 bytes OsmAnd/assets/server/images/markers-soft.png | Bin 0 -> 41226 bytes .../assets/server/images/markers-soft@2x.png | Bin 0 -> 66408 bytes OsmAnd/assets/server/scripts/go.js | 273 ++++ .../server/scripts/leaflet.awesome-markers.js | 134 ++ .../scripts/leaflet.awesome-markers.min.js | 7 + .../osmand/plus/activities/MapActivity.java | 2 + .../plus/activities/ServerActivity.java | 106 ++ .../src/net/osmand/plus/server/ApiRouter.java | 266 ++++ .../plus/server/ServerSessionHandler.java | 30 + .../osmand/plus/server/map/LayersDraw.java | 124 ++ .../plus/server/map/MapMarkersMiniLayer.java | 646 ++++++++ .../map/MapMarkersWidgetsMiniFactory.java | 306 ++++ .../plus/server/map/MapTextMiniLayer.java | 223 +++ .../plus/server/map/MapTileMiniLayer.java | 276 ++++ .../plus/server/map/MapVectorMiniLayer.java | 233 +++ .../osmand/plus/server/map/MyCustomLayer.java | 35 + .../plus/server/map/OsmandMapMiniLayer.java | 482 ++++++ .../server/map/OsmandMapTileMiniView.java | 1314 +++++++++++++++++ .../plus/server/map/POIMapLayerMini.java | 471 ++++++ .../osmand/plus/views/OsmandMapTileView.java | 44 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54417 bytes 29 files changed, 5177 insertions(+), 6 deletions(-) create mode 100644 OsmAnd/assets/server/css/leaflet.awesome-markers.css create mode 100644 OsmAnd/assets/server/go.html create mode 100644 OsmAnd/assets/server/images/markers-matte.png create mode 100644 OsmAnd/assets/server/images/markers-matte@2x.png create mode 100644 OsmAnd/assets/server/images/markers-plain.png create mode 100644 OsmAnd/assets/server/images/markers-shadow.png create mode 100644 OsmAnd/assets/server/images/markers-shadow@2x.png create mode 100644 OsmAnd/assets/server/images/markers-soft.png create mode 100644 OsmAnd/assets/server/images/markers-soft@2x.png create mode 100644 OsmAnd/assets/server/scripts/go.js create mode 100644 OsmAnd/assets/server/scripts/leaflet.awesome-markers.js create mode 100644 OsmAnd/assets/server/scripts/leaflet.awesome-markers.min.js create mode 100644 OsmAnd/src/net/osmand/plus/activities/ServerActivity.java create mode 100644 OsmAnd/src/net/osmand/plus/server/ApiRouter.java create mode 100644 OsmAnd/src/net/osmand/plus/server/ServerSessionHandler.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/LayersDraw.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/MapMarkersMiniLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/MapMarkersWidgetsMiniFactory.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/MapTextMiniLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/MapTileMiniLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/MapVectorMiniLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/MyCustomLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/OsmandMapMiniLayer.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/OsmandMapTileMiniView.java create mode 100644 OsmAnd/src/net/osmand/plus/server/map/POIMapLayerMini.java diff --git a/OsmAnd-java/src/main/java/net/osmand/data/RotatedTileBox.java b/OsmAnd-java/src/main/java/net/osmand/data/RotatedTileBox.java index 57c0080625..2219a0a6d6 100644 --- a/OsmAnd-java/src/main/java/net/osmand/data/RotatedTileBox.java +++ b/OsmAnd-java/src/main/java/net/osmand/data/RotatedTileBox.java @@ -2,6 +2,8 @@ package net.osmand.data; import net.osmand.util.MapUtils; +import java.util.Objects; + public class RotatedTileBox { /// primary fields @@ -69,6 +71,57 @@ public class RotatedTileBox { } } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RotatedTileBox tileBox = (RotatedTileBox) o; + return this.compare(tileBox.lat, lat) && + this.compare(tileBox.lon, lon) && + this.compare(tileBox.rotate, rotate) && + this.compare(tileBox.density, density) && + zoom == tileBox.zoom && + this.compare(tileBox.mapDensity, mapDensity) && + this.compare(tileBox.zoomAnimation, zoomAnimation) && + this.compare(tileBox.zoomFloatPart, zoomFloatPart) && + cx == tileBox.cx && + cy == tileBox.cy && + pixWidth == tileBox.pixWidth && + pixHeight == tileBox.pixHeight && + this.compare(tileBox.zoomFactor, zoomFactor) && + this.compare(tileBox.rotateCos, rotateCos) && + this.compare(tileBox.rotateSin, rotateSin) && + this.compare(tileBox.oxTile, oxTile) && + this.compare(tileBox.oyTile, oyTile); + } + + private double E = 0.0001; + + private boolean compare(float lon, float lon1) { + return Math.abs(lon1-lon) < E; + } + + private boolean compare(double lon, double lon1) { + return Math.abs(lon1-lon) < E; + } + + @Override + public int hashCode() { + int result = 1 + (int)lat + + 3* (int)(lon*1/E) + + 5* (int)(rotate*1/E) + + 7* (int)(density*1/E) + + 11* zoom + + 13* (int)(mapDensity*1/E) + + 17* (int)(zoomAnimation*1/E) + + 19* (int)(zoomFloatPart*1/E) + + 23* cx + + 29* cy + + 31* pixWidth + + 37* pixHeight; + return result; + } + public void calculateDerivedFields() { zoomFactor = Math.pow(2, zoomAnimation + zoomFloatPart) * 256 * mapDensity; double rad = Math.toRadians(this.rotate); diff --git a/OsmAnd/assets/server/css/leaflet.awesome-markers.css b/OsmAnd/assets/server/css/leaflet.awesome-markers.css new file mode 100644 index 0000000000..266b57271e --- /dev/null +++ b/OsmAnd/assets/server/css/leaflet.awesome-markers.css @@ -0,0 +1,57 @@ +/* +Author: L. Voogdt +License: MIT +Version: 1.0 +*/ + +/* Marker setup */ +.awesome-marker { + margin-top:-42px; + margin-left: -17px; + position:absolute; + left:0; + top:0; + display: block; + text-align: center; +} + +.awesome-marker .material-icons { + font-size: 17px; +} + +.awesome-marker-shadow { + background: url('/images/markers-shadow.png') no-repeat 0 0; + width: 36px; + height: 16px; +} + +/* Retina displays */ +@media (min--moz-device-pixel-ratio: 1.5),(-o-min-device-pixel-ratio: 3/2), +(-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5),(min-resolution: 1.5dppx) { + .awesome-marker-shadow { + background-image: url('/images/markers-shadow@2x.png'); + background-size: 35px 16px; + } +} + +.awesome-marker i { + color: #333; + margin-top: 10px; + display: inline-block; + font-size: 14px; +} + +.awesome-marker .icon-white { + color: #fff; +} + +/* Colors */ + +.awesome-marker-background { + fill: #fff; +} + +.awesome-marker:hover { + top: 2px; + opacity: 0.9; +} diff --git a/OsmAnd/assets/server/go.html b/OsmAnd/assets/server/go.html new file mode 100644 index 0000000000..8837fdbcd2 --- /dev/null +++ b/OsmAnd/assets/server/go.html @@ -0,0 +1,101 @@ + + + + + + + + + + + OsmAnd - Offline Mobile Maps and Navigation + + + + + + + + + + + + + + + + +
+
+ +
+
LAT
+
LON
+
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/OsmAnd/assets/server/images/markers-matte.png b/OsmAnd/assets/server/images/markers-matte.png new file mode 100644 index 0000000000000000000000000000000000000000..178258665dd06a9e387d85d48553beef8161650c GIT binary patch literal 14323 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 literal 0 HcmV?d00001 diff --git a/OsmAnd/assets/server/images/markers-matte@2x.png b/OsmAnd/assets/server/images/markers-matte@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c981244dd3bba2fa7eb186253fdad64d8a2fdc06 GIT binary patch literal 31113 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 O1I&%BPZk-vNBs{Ev@NLs literal 0 HcmV?d00001 diff --git a/OsmAnd/assets/server/images/markers-shadow.png b/OsmAnd/assets/server/images/markers-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..33cf95504706aa9d06ad40dbcd2fa168cbd43d13 GIT binary patch literal 535 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 aAw|UvuR9IAZi0KdKr~f#RX!_ONBti%eZQ*! literal 0 HcmV?d00001 diff --git a/OsmAnd/assets/server/images/markers-soft@2x.png b/OsmAnd/assets/server/images/markers-soft@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..540ce63759f8a8c7c46d136c14a4392c5802d635 GIT binary patch literal 66408 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 0){ + return value; + } + return null; + }, + 'isIOS':function(){ + return /(iPad|iPhone|iPod)/g.test( navigator.userAgent ); + }, + 'redirect':function(newUrl){ + document.location = newUrl; + } +}; + +var goMap = { + 'config':{ + 'containerid': 'gocontainer', + 'defaults':{ + 'lat':50.27, + 'lon':30.30, + 'zoom':13 + } + }, + 'utils':{ + 'getPointFromUrl':function(){ + let point = {}; + point.lat = requestUtils.getParamValue('lat'); + point.lon = requestUtils.getParamValue('lon'); + point.zoom = requestUtils.getParamValue('z'); + return point; + }, + 'isPointComplete':function(point){ + if (!point.lat || !point.lon){ + return false; + } + return true; + }, + 'extendPoint':function(initialPoint, newPoint){ + let point={}; + point.lat=newPoint.lat; + if (!point.lat || point.lat == null){ + point.lat = initialPoint.lat; + } + point.lon=newPoint.lon; + if (!point.lon || point.lon == null){ + point.lon = initialPoint.lon; + } + point.zoom=newPoint.zoom; + if (!point.zoom || point.zoom == null){ + point.zoom = initialPoint.zoom; + } + return point; + } + }, + 'init': function(config){ + if (config && typeof (config) == 'object') { + $.extend(goMap.config, config); + } + goMap.$container = $('#' + goMap.config.containerid); + goMap.$footer = goMap.$container.find('.gofooter'); + goMap.$latitude = goMap.$container.find('.latitude'); + goMap.$longitude = goMap.$container.find('.longitude'); + + let inputPoint = goMap.utils.getPointFromUrl(); + goMap.point = goMap.utils.extendPoint(goMap.config.defaults, inputPoint); + goMap.refreshCoordinates(); + + goMap.map =$.mapwidget(); + goMap.map.showPoint(goMap.point); + + let inputComplete = goMap.utils.isPointComplete(inputPoint); + if (inputComplete){ + goMap.map.addMarker(goMap.point); + } + goMap.point = goMap.utils.getPointFromUrl(); + }, + 'refreshCoordinates':function(){ + goMap.$latitude.text(goMap.point.lat); + goMap.$longitude.text(goMap.point.lon); + } +}; + +function toColor(num) { + num >>>= 0; + var b = num & 0xFF, + g = (num & 0xFF00) >>> 8, + r = (num & 0xFF0000) >>> 16, + a = ( (num & 0xFF000000) >>> 24 ) / 255 ; + return "rgba(" + [r, g, b, a].join(",") + ")"; + } + +(function($) { + $.mapwidget = function(config) { + var loc = goMap.point.lat + '/' + goMap.point.lon; + var lparams = '?mlat='+goMap.point.lat + '&mlon=' + goMap.point.lon; + var mapobj={ + config: $.extend({ + 'mapid':'map', + 'maxzoom':20, + 'maxnativezoom':19, + 'sourceurl':'https://tile.osmand.net/hd/{z}/{x}/{y}.png', + 'attribution':'© OpenStreetMap contributors' + }, config), + init:function(){ + mapobj.map = L.map(mapobj.config.mapid); + L.tileLayer(mapobj.config.sourceurl, { + attribution: mapobj.config.attribution, + maxZoom: mapobj.config.maxzoom, + maxNativeZoom: mapobj.config.maxnativezoom + }).addTo(mapobj.map); + }, + showPoint:function(point){ + mapobj.map.setView([point.lat, point.lon], point.zoom); + }, + addMarker:function(point){ + L.marker([point.lat, point.lon]).addTo(mapobj.map); + }, + addPopupMarker:function(favorite,onClickEvent){ + window.point = favorite; + var point = {}; + point.lat = favorite.latitude; + point.lon = favorite.longitude; + var popup = L.popup().setContent( + "name: " + favorite.name + "
" + + "address: " + favorite.address + "
" + + "category: " + favorite.category); + var customMarker = L.AwesomeMarkers.icon({ + icon: 'home', + markerColor: toColor(favorite.color), + iconColor: toColor(favorite.color) + }); + L.marker([point.lat, point.lon],{icon: customMarker}) + .bindPopup(popup) + .addTo(mapobj.map) + .on('click', function(e) { + onClickEvent(e); + }); + } + }; + mapobj.init(); + return { + showPoint: mapobj.showPoint, + addMarker: mapobj.addMarker, + addPopupMarker: mapobj.addPopupMarker + }; + }; +})(jQuery); + +(function($) { + $.timer=function(config){ + var timerobj={ + config: $.extend({ + 'timeoutInMs':300, + 'maxActionDelayInMs':2000, + 'action':function(){}, + 'actionparams':null + }, config), + init:function(){ + timerobj.timer = null; + timerobj.startDate = null; + }, + start:function(){ + timerobj.cancel(); + timerobj.startDate = new Date(); + timerobj.timer=setTimeout(timerobj.onTimer, timerobj.config.timeoutInMs); + }, + cancel:function(){ + if (timerobj.timer != null){ + clearTimeout(timerobj.timer); + timerobj.timer = null; + timerobj.startDate = null; + } + }, + onTimer:function(){ + timerobj.timer= null; + let now = new Date(); + if(now - timerobj.startDate < timerobj.config.maxActionDelayInMs){ + timerobj.config.action(timerobj.config.actionparams); + } + } + }; + timerobj.init(); + return { + start:timerobj.start, + cancel:timerobj.cancel + }; + }; +})(jQuery); + +var iosAppRedirect = { + config:{ + appPrefix:'osmandmaps://', + containerid:'gocontainer', + cookieName:'OsmAndInstalled', + cookieNoExpirationTimeoutInDays:30 + }, + init:function(config){ + if (config && typeof (config) == 'object') { + $.extend(iosAppRedirect.config, config); + } + + if (!requestUtils.isIOS()){ + return; + } + iosAppRedirect.$container = $('#' + iosAppRedirect.config.containerid); + iosAppRedirect.$overlay = iosAppRedirect.$container.find('.overlay'); + iosAppRedirect.$popup = iosAppRedirect.$container.find('.popup'); + iosAppRedirect.$yesBtn = iosAppRedirect.$container.find('.yes'); + iosAppRedirect.$noBtn = iosAppRedirect.$container.find('.no'); + iosAppRedirect.$cancelBtn = iosAppRedirect.$container.find('.cancel'); + iosAppRedirect.applestorelink = iosAppRedirect.$container.find('.gobadges .apple a').attr('href'); + iosAppRedirect.applink = iosAppRedirect.config.appPrefix + document.location.search; + + + if (iosAppRedirect.isAppInstalled() === "yes"){ + iosAppRedirect.redirectToApp(); + return; + } + if (iosAppRedirect.isAppInstalled() === "no"){ + return; + } + + iosAppRedirect.$yesBtn.on('click', function(){ + iosAppRedirect.redirectToApp(); + iosAppRedirect.closePopup(); + }); + + iosAppRedirect.$noBtn.on('click', function(){ + iosAppRedirect.setCookie(true); + iosAppRedirect.closePopup(); + window.open(iosAppRedirect.applestorelink , '_blank'); + }); + + iosAppRedirect.$cancelBtn.on('click', function(){ + iosAppRedirect.setCookie(false); + iosAppRedirect.closePopup(); + }); + iosAppRedirect.openPopup(); + }, + isAppInstalled:function(){ + return Cookies.get('OsmAndInstalled'); + }, + redirectToApp:function(){ + iosAppRedirect.timer = $.timer({action:iosAppRedirect.clearCookie}); + iosAppRedirect.timer.start(); + requestUtils.redirect(iosAppRedirect.applink); + }, + setCookie:function(appInstalled){ + if (appInstalled === true){ + Cookies.set(iosAppRedirect.config.cookieName, "yes"); + }else{ + Cookies.set(iosAppRedirect.config.cookieName, "no", { expires: iosAppRedirect.config.cookieNoExpirationTimeoutInDays }); + } + }, + clearCookie:function(){ + Cookies.remove('OsmAndInstalled'); + }, + openPopup:function(){ + iosAppRedirect.$overlay.show(); + iosAppRedirect.$popup.show(); + }, + closePopup:function(){ + iosAppRedirect.$overlay.hide(); + iosAppRedirect.$popup.hide(); + } +}; + + $( document ).ready(function() { + goMap.init(); + iosAppRedirect.init(); + }); diff --git a/OsmAnd/assets/server/scripts/leaflet.awesome-markers.js b/OsmAnd/assets/server/scripts/leaflet.awesome-markers.js new file mode 100644 index 0000000000..445fdc6336 --- /dev/null +++ b/OsmAnd/assets/server/scripts/leaflet.awesome-markers.js @@ -0,0 +1,134 @@ +/* + Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons + (c) 2012-2013, Lennard Voogdt + + http://leafletjs.com + https://github.com/lvoogdt +*/ + +/*global L*/ + +(function (window, document, undefined) { + "use strict"; + /* + * Leaflet.AwesomeMarkers assumes that you have already included the Leaflet library. + */ + + L.AwesomeMarkers = {}; + + L.AwesomeMarkers.version = '2.0.1'; + + L.AwesomeMarkers.Icon = L.Icon.extend({ + options: { + shadowAnchor: [10, 12], + shadowSize: [36, 16], + className: 'awesome-marker', + icon: 'block', + markerColor: 'white', + iconColor: 'white' + }, + + initialize: function (options) { + options = L.Util.setOptions(this, options); + }, + + createIcon: function () { + var options = L.Util.setOptions(this); + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + var path = document.createElementNS('http://www.w3.org/2000/svg', "path"); + var backgroundCircle = document.createElementNS('http://www.w3.org/2000/svg', "circle"); + var icongroup = document.createElementNS('http://www.w3.org/2000/svg', "g"); + var icon = document.createElementNS('http://www.w3.org/2000/svg', "text"); + + svg.setAttribute('width', '31'); + svg.setAttribute('height', '42'); + svg.setAttribute('class', 'awesome-marker'); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + + backgroundCircle.setAttribute('cx', '15.5'); + backgroundCircle.setAttribute('cy', '15'); + backgroundCircle.setAttribute('r', '11'); + backgroundCircle.setAttribute('fill', options.markerColor); + + path.setAttributeNS(null, "d", "M15.6,1c-7.7,0-14,6.3-14,14c0,10.5,14,26,14,26s14-15.5,14-26C29.6,7.3,23.3,1,15.6,1z"); + //path.setAttribute('class', 'awesome-marker-background'); + path.setAttribute('stroke', 'white'); + path.setAttribute('style', 'fill:' + options.markerColor) + + icon.textContent = options.icon; + icon.setAttribute('x', '7'); + icon.setAttribute('y', '23'); + icon.setAttribute('class', 'material-icons'); + icon.setAttribute('fill', options.iconColor); + icon.setAttribute('font-family', 'Material Icons'); + + svg.appendChild(path); + svg.appendChild(backgroundCircle); + icongroup.appendChild(icon); + svg.appendChild(icongroup); + + return svg; + }, + + _createInner: function() { + var iconClass, iconSpinClass = "", iconColorClass = "", iconColorStyle = "", options = this.options; + + if (options.spin && typeof options.spinClass === "string") { + iconSpinClass = options.spinClass; + } + + if (options.iconColor) { + if (options.iconColor === 'white' || options.iconColor === 'black') { + iconColorClass = "icon-" + options.iconColor; + } else { + iconColorStyle = "style='color: " + options.iconColor + "' "; + } + } + //return "" + return options.extraClasses + " " + iconClass + " " + iconSpinClass + " " + iconColorClass; + }, + + _setIconStyles: function (img, name) { + var options = this.options, + size = L.point(options[name === 'shadow' ? 'shadowSize' : 'iconSize']), + anchor; + + if (name === 'shadow') { + anchor = L.point(options.shadowAnchor || options.iconAnchor); + } else { + anchor = L.point(options.iconAnchor); + } + + if (!anchor && size) { + anchor = size.divideBy(2, true); + } + + img.className = 'awesome-marker-' + name + ' ' + options.className; + + if (anchor) { + img.style.marginLeft = (-anchor.x) + 'px'; + img.style.marginTop = (-anchor.y) + 'px'; + } + + if (size) { + img.style.width = size.x + 'px'; + img.style.height = size.y + 'px'; + } + }, + + createShadow: function () { + var div = document.createElement('div'); + + this._setIconStyles(div, 'shadow'); + return div; + } + }); + + L.AwesomeMarkers.icon = function (options) { + return new L.AwesomeMarkers.Icon(options); + }; + +}(this, document)); + + + diff --git a/OsmAnd/assets/server/scripts/leaflet.awesome-markers.min.js b/OsmAnd/assets/server/scripts/leaflet.awesome-markers.min.js new file mode 100644 index 0000000000..376e57e68d --- /dev/null +++ b/OsmAnd/assets/server/scripts/leaflet.awesome-markers.min.js @@ -0,0 +1,7 @@ +/* + Leaflet.AwesomeMarkers, a plugin that adds colorful iconic markers for Leaflet, based on the Font Awesome icons + (c) 2012-2013, Lennard Voogdt + + http://leafletjs.com + https://github.com/lvoogdt +*//*global L*/(function(e,t,n){"use strict";L.AwesomeMarkers={};L.AwesomeMarkers.version="2.0.1";L.AwesomeMarkers.Icon=L.Icon.extend({options:{iconSize:[35,45],iconAnchor:[17,42],popupAnchor:[1,-32],shadowAnchor:[10,12],shadowSize:[36,16],className:"awesome-marker",prefix:"glyphicon",spinClass:"fa-spin",icon:"home",markerColor:"blue",iconColor:"white"},initialize:function(e){e=L.Util.setOptions(this,e)},createIcon:function(){var e=t.createElement("div"),n=this.options;n.icon&&(e.innerHTML=this._createInner());n.bgPos&&(e.style.backgroundPosition=-n.bgPos.x+"px "+ -n.bgPos.y+"px");this._setIconStyles(e,"icon-"+n.markerColor);return e},_createInner:function(){var e,t="",n="",r="",i=this.options;i.icon.slice(0,i.prefix.length+1)===i.prefix+"-"?e=i.icon:e=i.prefix+"-"+i.icon;i.spin&&typeof i.spinClass=="string"&&(t=i.spinClass);i.iconColor&&(i.iconColor==="white"||i.iconColor==="black"?n="icon-"+i.iconColor:r="style='color: "+i.iconColor+"' ");return""},_setIconStyles:function(e,t){var n=this.options,r=L.point(n[t==="shadow"?"shadowSize":"iconSize"]),i;t==="shadow"?i=L.point(n.shadowAnchor||n.iconAnchor):i=L.point(n.iconAnchor);!i&&r&&(i=r.divideBy(2,!0));e.className="awesome-marker-"+t+" "+n.className;if(i){e.style.marginLeft=-i.x+"px";e.style.marginTop=-i.y+"px"}if(r){e.style.width=r.x+"px";e.style.height=r.y+"px"}},createShadow:function(){var e=t.createElement("div");this._setIconStyles(e,"shadow");return e}});L.AwesomeMarkers.icon=function(e){return new L.AwesomeMarkers.Icon(e)}})(this,document); diff --git a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java index f71a6c0fb6..f58acb8591 100644 --- a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java @@ -136,6 +136,7 @@ import net.osmand.plus.routing.TransportRoutingHelper.TransportRouteCalculationP import net.osmand.plus.search.QuickSearchDialogFragment; import net.osmand.plus.search.QuickSearchDialogFragment.QuickSearchTab; import net.osmand.plus.search.QuickSearchDialogFragment.QuickSearchType; +import net.osmand.plus.server.ApiRouter; import net.osmand.plus.settings.backend.ApplicationMode; import net.osmand.plus.settings.backend.OsmAndAppCustomization.OsmAndAppCustomizationListener; import net.osmand.plus.settings.backend.OsmandSettings; @@ -309,6 +310,7 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven int h = dm.heightPixels - statusBarHeight; mapView = new OsmandMapTileView(this, w, h); + ApiRouter.mapActivity = this; if (app.getAppInitializer().checkAppVersionChanged() && WhatsNewDialogFragment.SHOW) { SecondSplashScreenFragment.SHOW = false; WhatsNewDialogFragment.SHOW = false; diff --git a/OsmAnd/src/net/osmand/plus/activities/ServerActivity.java b/OsmAnd/src/net/osmand/plus/activities/ServerActivity.java new file mode 100644 index 0000000000..eda412c599 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/activities/ServerActivity.java @@ -0,0 +1,106 @@ +package net.osmand.plus.activities; + +import android.net.TrafficStats; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.StrictMode; +import android.text.format.Formatter; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.server.OsmAndHttpServer; + +import java.io.IOException; + +public class ServerActivity extends AppCompatActivity { + private boolean initialized = false; + private OsmAndHttpServer server; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + enableStrictMode(); + super.onCreate(savedInstanceState); + setContentView(R.layout.server_activity); + findViewById(R.id.Button01).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!initialized) { + updateTextView("Click second button to deactivate server"); + initServer(); + } + } + }); + findViewById(R.id.Button03).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (initialized) { + updateTextView("Click first button to activate server"); + deInitServer(); + } + } + }); + } + + public static void enableStrictMode() { + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() + .penaltyLog() + .build()); + StrictMode.setVmPolicy( + new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .penaltyLog() + .build()); + } + + + private void updateTextView(String text) { + ((TextView) findViewById(R.id.TextView02)).setText(text); + } + + private void initServer() { + final int THREAD_ID = 10000; + TrafficStats.setThreadStatsTag(THREAD_ID); + OsmAndHttpServer.HOSTNAME = getDeviceAddress(); + try { + server = new OsmAndHttpServer(); + server.setAndroidApplication((OsmandApplication)this.getApplication()); + initialized = true; + updateTextView("Server started at: http://" + getDeviceAddress() + ":" + OsmAndHttpServer.PORT); + } catch (IOException e) { + Toast.makeText(this, + e.getLocalizedMessage(), + Toast.LENGTH_SHORT).show(); + e.printStackTrace(); + } + } + + private String getDeviceAddress() { + WifiManager wm = (WifiManager) this.getApplicationContext().getSystemService(WIFI_SERVICE); + String ip = Formatter.formatIpAddress(wm.getConnectionInfo().getIpAddress()); + return ip != null ? ip : "0.0.0.0"; + } + + private void deInitServer() { + if (server != null){ + server.closeAllConnections(); + server.stop(); + } + initialized = false; + } + + @Override + protected void onDestroy() { + deInitServer(); + super.onDestroy(); + } +} diff --git a/OsmAnd/src/net/osmand/plus/server/ApiRouter.java b/OsmAnd/src/net/osmand/plus/server/ApiRouter.java new file mode 100644 index 0000000000..a8e5ddc86d --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/ApiRouter.java @@ -0,0 +1,266 @@ +package net.osmand.plus.server; + +import android.graphics.Bitmap; +import android.util.Log; +import android.util.Pair; +import android.webkit.MimeTypeMap; +import com.google.gson.Gson; +import fi.iki.elonen.NanoHTTPD; +import net.osmand.data.FavouritePoint; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.views.OsmandMapLayer; +import net.osmand.plus.views.OsmandMapTileView; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.*; + +import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; + +public class ApiRouter implements OsmandMapTileView.IMapImageDrawListener { + private OsmandApplication androidContext; + + public OsmandApplication getAndroidContext() { + return androidContext; + } + + private final String FOLDER_NAME = "server"; + private Gson gson = new Gson(); + private Map endpoints = new HashMap<>(); + //change to weakreference + public static MapActivity mapActivity; + + public ApiRouter() { + initRoutes(); + } + + private void initRoutes() { + ApiEndpoint favorites = new ApiEndpoint(); + favorites.uri = "/favorites"; + favorites.apiCall = new ApiEndpoint.ApiCall() { + @Override + public NanoHTTPD.Response call(NanoHTTPD.IHTTPSession session) { + return newFixedLengthResponse(getFavoritesJson()); + } + }; + endpoints.put(favorites.uri, favorites); + + final ApiEndpoint tile = new ApiEndpoint(); + tile.uri = "/tile"; + tile.apiCall = new ApiEndpoint.ApiCall() { + @Override + public NanoHTTPD.Response call(NanoHTTPD.IHTTPSession session) { + try { + return tileApiCall(session); + } catch (Exception e) { + e.printStackTrace(); + } + return ErrorResponses.response500; + } + }; + endpoints.put(tile.uri, tile); + } + + ExecutorService executor = Executors.newFixedThreadPool(3); + + Map hashMap = new HashMap<>(); + Map map = Collections.synchronizedMap(hashMap); + + private synchronized NanoHTTPD.Response tileApiCall(NanoHTTPD.IHTTPSession session) { + int zoom = 0; + double lat = 0;//50.901430; + double lon = 0;//34.801775; + try { + String fullUri = session.getUri().replace("/tile/", ""); + Scanner s = new Scanner(fullUri).useDelimiter("/"); + zoom = s.nextInt(); + lat = s.nextDouble(); + lon = s.nextDouble(); + } catch (Exception e) { + e.printStackTrace(); + return ErrorResponses.response500; + } + mapActivity.getMapView().setMapImageDrawListener(this); + Future> future; + final RotatedTileBox rotatedTileBox = new RotatedTileBox.RotatedTileBoxBuilder() + .setLocation(lat, lon) + .setZoom(zoom) + .setPixelDimensions(512, 512, 0.5f, 0.5f).build(); + future = executor.submit(new Callable>() { + @Override + public Pair call() throws Exception { + Bitmap bmp; + while ((bmp = map.get(rotatedTileBox)) == null) { + Thread.sleep(1000); + } + return Pair.create(rotatedTileBox, bmp); + } + }); + mapActivity.getMapView().setCurrentRotatedTileBox(rotatedTileBox); + try { + Pair pair = future.get(); + Bitmap bitmap = pair.second;// mapActivity.getMapView().currentCanvas; + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + byte[] byteArray = stream.toByteArray(); + ByteArrayInputStream str = new ByteArrayInputStream(byteArray); + return newFixedLengthResponse( + NanoHTTPD.Response.Status.OK, + "image/png", + str, + str.available()); + } catch (ExecutionException e) { + e.printStackTrace(); + return ErrorResponses.response500; + } catch (InterruptedException e) { + e.printStackTrace(); + return ErrorResponses.response500; + } + } + + public void setAndroidContext(OsmandApplication androidContext) { + this.androidContext = androidContext; + } + + public NanoHTTPD.Response route(NanoHTTPD.IHTTPSession session) { + Log.d("SERVER", "URI: " + session.getUri()); + String uri = session.getUri(); + if (uri.equals("/")) return getStatic("/go.html"); + if (uri.contains("/scripts/") || + uri.contains("/images/") || + uri.contains("/css/") || + uri.contains("/fonts/") || + uri.contains("/favicon.ico") + ) return getStatic(uri); + if (isApiUrl(uri)) { + return routeApi(session); + } else { + return routeContent(session); + } + } + + private NanoHTTPD.Response routeApi(NanoHTTPD.IHTTPSession session) { + String uri = session.getUri(); + //TODO rewrite + if (uri.contains("tile")) { + return endpoints.get("/tile").apiCall.call(session); + } + ApiEndpoint endpoint = endpoints.get(uri); + if (endpoint != null) { + return endpoint.apiCall.call(session); + } + return ErrorResponses.response404; + } + + private boolean isApiUrl(String uri) { + for (String endpoint : endpoints.keySet()) { + //TODO rewrite contains + if (endpoint.equals(uri) || uri.contains("tile")) return true; + } + return false; + } + + private NanoHTTPD.Response routeContent(NanoHTTPD.IHTTPSession session) { + String url = session.getUri(); + //add index page + String responseText = getHtmlPage(url); + if (responseText != null) { + return newFixedLengthResponse(responseText); + } else { + return ErrorResponses.response404; + } + } + + public NanoHTTPD.Response getStatic(String uri) { + InputStream is = null; + String mimeType = parseMimeType(uri); + if (androidContext != null) { + try { + is = androidContext.getAssets().open(FOLDER_NAME + uri); + if (is.available() == 0) { + return ErrorResponses.response404; + } + return newFixedLengthResponse( + NanoHTTPD.Response.Status.OK, + mimeType, + is, + is.available()); + } catch (IOException e) { + return ErrorResponses.response404; + } + } + return ErrorResponses.response500; + } + + private String parseMimeType(String url) { + String type = null; + if (url.endsWith(".js")) return "text/javascript"; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + private String readHTMLFromFile(String filename) { + StringBuilder sb = new StringBuilder(); + try { + InputStream is = androidContext.getAssets().open(FOLDER_NAME + filename); + BufferedReader br = new BufferedReader(new InputStreamReader(is, + Charset.forName("UTF-8"))); + String str; + while ((str = br.readLine()) != null) { + sb.append(str); + } + br.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return sb.toString(); + } + + public String getHtmlPage(String name) { + String responseText = ""; + if (androidContext != null) { + responseText = readHTMLFromFile(name); + } + if (responseText == null) { + return null; + } + return responseText; + } + + private String getFavoritesJson() { + List points = androidContext.getFavorites().getFavouritePoints(); + StringBuilder text = new StringBuilder(); + for (FavouritePoint p : points) { + String json = jsonFromFavorite(p); + text.append(json); + text.append(","); + } + return "[" + text.substring(0, text.length() - 1) + "]"; + } + + private String jsonFromFavorite(FavouritePoint favouritePoint) { + return gson.toJson(favouritePoint); + } + + @Override + public void onDraw(RotatedTileBox viewport, Bitmap bmp) { + this.map.put(viewport, bmp); + } + + static class ErrorResponses { + static NanoHTTPD.Response response404 = + newFixedLengthResponse(NanoHTTPD.Response.Status.NOT_FOUND, + NanoHTTPD.MIME_PLAINTEXT, "404 Not Found"); + + static NanoHTTPD.Response response500 = + newFixedLengthResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, + NanoHTTPD.MIME_PLAINTEXT, "500 Internal Server Error"); + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/server/ServerSessionHandler.java b/OsmAnd/src/net/osmand/plus/server/ServerSessionHandler.java new file mode 100644 index 0000000000..13d54fcab8 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/ServerSessionHandler.java @@ -0,0 +1,30 @@ +package net.osmand.plus.server; + +import net.osmand.plus.OsmandApplication; + +import fi.iki.elonen.NanoHTTPD; +import net.osmand.plus.activities.MapActivity; + +public class ServerSessionHandler { + private OsmandApplication androidApplication; + + private ApiRouter router = new ApiRouter(); + + public OsmandApplication getAndroidApplication() { + return androidApplication; + } + + public void setAndroidApplication(OsmandApplication androidApplication) { + this.androidApplication = androidApplication; + router.setAndroidContext(androidApplication); + } + + public void setMapActivity(MapActivity activity) { + //todo + router.mapActivity = activity; + } + + public NanoHTTPD.Response handle(NanoHTTPD.IHTTPSession session) { + return router.route(session); + } +} diff --git a/OsmAnd/src/net/osmand/plus/server/map/LayersDraw.java b/OsmAnd/src/net/osmand/plus/server/map/LayersDraw.java new file mode 100644 index 0000000000..77997d48df --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/LayersDraw.java @@ -0,0 +1,124 @@ +package net.osmand.plus.server.map; + +import android.graphics.Canvas; +import net.osmand.data.RotatedTileBox; +import net.osmand.map.ITileSource; +import net.osmand.map.TileSourceManager; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.render.MapVectorLayer; +import net.osmand.plus.routing.RoutingHelper; +import net.osmand.plus.views.MapTileLayer; +import net.osmand.plus.views.layers.FavouritesLayer; +import net.osmand.plus.views.layers.GPXLayer; +import net.osmand.plus.views.layers.MapTextLayer; +import net.osmand.plus.views.layers.RouteLayer; + +public class LayersDraw { + public static void createLayers(OsmandApplication app, Canvas canvas, final OsmandMapTileMiniView mapView) { + RoutingHelper routingHelper = app.getRoutingHelper(); + // first create to make accessible + MapTextLayer mapTextLayer = new MapTextLayer(); + // 5.95 all labels + //mapView.addLayer(mapTextLayer, 5.95f); + // 8. context menu layer + //ContextMenuLayer contextMenuLayer = new ContextMenuLayer(app); + //mapView.addLayer(contextMenuLayer, 8); + // mapView.addLayer(underlayLayer, -0.5f); + RotatedTileBox currentTileBlock = mapView.getCurrentRotatedTileBox(); +// RotatedTileBox currentTileBlock = new RotatedTileBox.RotatedTileBoxBuilder() +// .setLocation(50.901430, 34.801775) +// .setZoom(15) +// .setPixelDimensions(canvas.getWidth(), canvas.getHeight(), 0.5f, 0.5f).build(); + + MapTileMiniLayer mapTileLayer = new MapTileMiniLayer(true); + mapView.addLayer(mapTileLayer, 0.0f); + + ITileSource map = TileSourceManager.getMapillaryVectorSource(); + mapTileLayer.setMap(map); + mapTileLayer.drawTileMap(canvas,currentTileBlock); + //mapView.setMainLayer(mapTileLayer); + // 0.5 layer + MapVectorMiniLayer mapVectorLayer = new MapVectorMiniLayer(mapTileLayer, false); + mapView.addLayer(mapVectorLayer, 0.5f); +// mapVectorLayer.onPrepareBufferImage(canvas, +// currentTileBlock, +// new OsmandMapMiniLayer.DrawSettings(false)); + //DownloadedRegionsLayer downloadedRegionsLayer = new DownloadedRegionsLayer(activity); + //mapView.addLayer(downloadedRegionsLayer, 0.5f); + + // 0.9 gpx layer + GPXLayer gpxLayer = new GPXLayer(); + //mapView.addLayer(gpxLayer, 0.9f); + + // 1. route layer + RouteLayer routeLayer = new RouteLayer(routingHelper); + //mapView.addLayer(routeLayer, 1); + + // 2. osm bugs layer + // 3. poi layer + POIMapLayerMini poiMapLayer = new POIMapLayerMini(app); + mapView.addLayer(poiMapLayer, 3); + + poiMapLayer.onPrepareBufferImage(canvas, currentTileBlock, + new OsmandMapMiniLayer.DrawSettings(false)); + // 4. favorites layer + FavouritesLayer mFavouritesLayer = new FavouritesLayer(); + //mapView.addLayer(mFavouritesLayer, 4); + // 4.6 measurement tool layer + //MeasurementToolLayer measurementToolLayer = new MeasurementToolLayer(); + //mapView.addLayer(measurementToolLayer, 4.6f); + // 5. transport layer + //TransportStopsLayer transportStopsLayer = new TransportStopsLayer(activity); + //mapView.addLayer(transportStopsLayer, 5); + // 5.95 all text labels + // 6. point location layer + //PointLocationLayer locationLayer = new PointLocationLayer(activity.getMapViewTrackingUtilities()); + //mapView.addLayer(locationLayer, 6); + // 7. point navigation layer + //PointNavigationLayer navigationLayer = new PointNavigationLayer(activity); + //mapView.addLayer(navigationLayer, 7); + // 7.3 map markers layer + //MapMarkersMiniLayer mapMarkersLayer = new MapMarkersMiniLayer(app); + //mapView.addLayer(mapMarkersLayer, 7.3f); + //MyCustomLayer layer = new MyCustomLayer(); + //mapView.addLayer(layer,2); + + // 7.5 Impassible roads + //ImpassableRoadsLayer impassableRoadsLayer = new ImpassableRoadsLayer(activity); + //mapView.addLayer(impassableRoadsLayer, 7.5f); + // 7.8 ruler control layer + //RulerControlLayer rulerControlLayer = new RulerControlLayer(activity); + //mapView.addLayer(rulerControlLayer, 7.8f); + // 8. context menu layer + // 9. map info layer + //MapInfoLayer mapInfoLayer = new MapInfoLayer(activity, routeLayer); + //mapView.addLayer(mapInfoLayer, 9); + // 11. route info layer + //MapControlsLayer mapControlsLayer = new MapControlsLayer(activity); + //mapView.addLayer(mapControlsLayer, 11); + // 12. quick actions layer + //MapQuickActionLayer mapQuickActionLayer = new MapQuickActionLayer(activity, contextMenuLayer); + //mapView.addLayer(mapQuickActionLayer, 12); + //contextMenuLayer.setMapQuickActionLayer(mapQuickActionLayer); + //mapControlsLayer.setMapQuickActionLayer(mapQuickActionLayer); + +// StateChangedListener transparencyListener = new StateChangedListener() { +// @Override +// public void stateChanged(Integer change) { +// mapTileLayer.setAlpha(change); +// mapVectorLayer.setAlpha(change); +// mapView.refreshMap(); +// } +// }; + // app.getSettings().MAP_TRANSPARENCY.addListener(transparencyListener); + + + //OsmandPlugin.createLayers(mapView, activity); + //app.getAppCustomization().createLayers(mapView, activity); + //app.getAidlApi().registerMapLayers(activity); + + //return OsmandPlugin.createLayers(mapView, app); + //app.getAppCustomization().createLayers(mapView, activity); + //app.getAidlApi().registerMapLayers(activity); + } +} diff --git a/OsmAnd/src/net/osmand/plus/server/map/MapMarkersMiniLayer.java b/OsmAnd/src/net/osmand/plus/server/map/MapMarkersMiniLayer.java new file mode 100644 index 0000000000..31161b5d52 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/MapMarkersMiniLayer.java @@ -0,0 +1,646 @@ +package net.osmand.plus.server.map; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.PointF; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Message; +import android.text.TextPaint; +import android.text.TextUtils; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.google.android.material.snackbar.Snackbar; + +import net.osmand.GPXUtilities.TrkSegment; +import net.osmand.Location; +import net.osmand.data.Amenity; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.data.QuadPoint; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.MapMarkersHelper; +import net.osmand.plus.MapMarkersHelper.MapMarker; +import net.osmand.plus.OsmAndConstants; +import net.osmand.plus.OsmAndFormatter; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.R; +import net.osmand.plus.TargetPointsHelper.TargetPoint; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.base.MapViewTrackingUtilities; +import net.osmand.plus.views.OsmandMapLayer; +import net.osmand.plus.views.OsmandMapTileView; +import net.osmand.plus.views.Renderable; +import net.osmand.plus.views.layers.ContextMenuLayer; +import net.osmand.plus.views.layers.ContextMenuLayer.ApplyMovedObjectCallback; +import net.osmand.plus.views.layers.ContextMenuLayer.IContextMenuProvider; +import net.osmand.plus.views.layers.ContextMenuLayer.IContextMenuProviderSelection; +import net.osmand.plus.views.layers.geometry.GeometryWay; +import net.osmand.plus.views.mapwidgets.MapMarkersWidgetsFactory; +import net.osmand.util.MapUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MapMarkersMiniLayer extends OsmandMapMiniLayer implements IContextMenuProvider, + IContextMenuProviderSelection, ContextMenuLayer.IMoveObjectProvider { + + private static final long USE_FINGER_LOCATION_DELAY = 1000; + private static final int MAP_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 6; + protected static final int DIST_TO_SHOW = 80; + private OsmandApplication application; + + private OsmandMapTileMiniView view; + + private MapMarkersWidgetsMiniFactory widgetsFactory; + + private Paint bitmapPaint; + private Bitmap markerBitmapBlue; + private Bitmap markerBitmapGreen; + private Bitmap markerBitmapOrange; + private Bitmap markerBitmapRed; + private Bitmap markerBitmapYellow; + private Bitmap markerBitmapTeal; + private Bitmap markerBitmapPurple; + + private Paint bitmapPaintDestBlue; + private Paint bitmapPaintDestGreen; + private Paint bitmapPaintDestOrange; + private Paint bitmapPaintDestRed; + private Paint bitmapPaintDestYellow; + private Paint bitmapPaintDestTeal; + private Paint bitmapPaintDestPurple; + private Bitmap arrowLight; + private Bitmap arrowToDestination; + private Bitmap arrowShadow; + private float[] calculations = new float[2]; + + private final TextPaint textPaint = new TextPaint(); + private final RenderingLineAttributes lineAttrs = new RenderingLineAttributes("measureDistanceLine"); + private final RenderingLineAttributes textAttrs = new RenderingLineAttributes("rulerLineFont"); + private final RenderingLineAttributes planRouteAttrs = new RenderingLineAttributes("markerPlanRouteline"); + private TrkSegment route; + + private float textSize; + private int verticalOffset; + + private List tx = new ArrayList<>(); + private List ty = new ArrayList<>(); + private Path linePath = new Path(); + + private LatLon fingerLocation; + private boolean hasMoved; + private boolean moving; + private boolean useFingerLocation; + private GestureDetector longTapDetector; + private Handler handler; + + private ContextMenuLayer contextMenuLayer; + + private boolean inPlanRouteMode; + private boolean defaultAppMode = true; + + private List amenities = new ArrayList<>(); + + public MapMarkersMiniLayer(OsmandApplication app) { + this.application = app; + } + + public MapMarkersWidgetsMiniFactory getWidgetsFactory() { + return widgetsFactory; + } + + public boolean isInPlanRouteMode() { + return inPlanRouteMode; + } + + public void setInPlanRouteMode(boolean inPlanRouteMode) { + this.inPlanRouteMode = inPlanRouteMode; + } + + public void setDefaultAppMode(boolean defaultAppMode) { + this.defaultAppMode = defaultAppMode; + } + + private void initUI() { + bitmapPaint = new Paint(); + bitmapPaint.setAntiAlias(true); + bitmapPaint.setFilterBitmap(true); + markerBitmapBlue = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_blue); + markerBitmapGreen = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_green); + markerBitmapOrange = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_orange); + markerBitmapRed = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_red); + markerBitmapYellow = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_yellow); + markerBitmapTeal = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_teal); + markerBitmapPurple = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_purple); + + arrowLight = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_direction_arrow_p1_light); + arrowToDestination = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_direction_arrow_p2_color); + arrowShadow = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_marker_direction_arrow_p3_shadow); + bitmapPaintDestBlue = createPaintDest(R.color.marker_blue); + bitmapPaintDestGreen = createPaintDest(R.color.marker_green); + bitmapPaintDestOrange = createPaintDest(R.color.marker_orange); + bitmapPaintDestRed = createPaintDest(R.color.marker_red); + bitmapPaintDestYellow = createPaintDest(R.color.marker_yellow); + bitmapPaintDestTeal = createPaintDest(R.color.marker_teal); + bitmapPaintDestPurple = createPaintDest(R.color.marker_purple); + + widgetsFactory = new MapMarkersWidgetsMiniFactory(application); + + //contextMenuLayer = view.getLayerByClass(ContextMenuLayer.class); + + textSize = application.getResources().getDimensionPixelSize(R.dimen.guide_line_text_size); + verticalOffset = application.getResources().getDimensionPixelSize(R.dimen.guide_line_vertical_offset); + } + + private Paint createPaintDest(int colorId) { + Paint paint = new Paint(); + paint.setDither(true); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + int color = ContextCompat.getColor(application, colorId); + paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); + return paint; + } + + private Paint getMarkerDestPaint(int colorIndex) { + switch (colorIndex) { + case 0: + return bitmapPaintDestBlue; + case 1: + return bitmapPaintDestGreen; + case 2: + return bitmapPaintDestOrange; + case 3: + return bitmapPaintDestRed; + case 4: + return bitmapPaintDestYellow; + case 5: + return bitmapPaintDestTeal; + case 6: + return bitmapPaintDestPurple; + default: + return bitmapPaintDestBlue; + } + } + + private Bitmap getMapMarkerBitmap(int colorIndex) { + switch (colorIndex) { + case 0: + return markerBitmapBlue; + case 1: + return markerBitmapGreen; + case 2: + return markerBitmapOrange; + case 3: + return markerBitmapRed; + case 4: + return markerBitmapYellow; + case 5: + return markerBitmapTeal; + case 6: + return markerBitmapPurple; + default: + return markerBitmapBlue; + } + } + + public void setRoute(TrkSegment route) { + this.route = route; + } + + @Override + public void initLayer(OsmandMapTileMiniView view) { + this.view = view; + //handler = new Handler(); + initUI(); +// longTapDetector = new GestureDetector(view.getContext(), new GestureDetector.SimpleOnGestureListener() { +// @Override +// public void onLongPress(MotionEvent e) { +// cancelFingerAction(); +// } +// }); + } + + @Override + public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings nightMode) { + OsmandApplication app = application; + OsmandSettings settings = app.getSettings(); + if (!settings.SHOW_MAP_MARKERS.get()) { + return; + } + + Location myLoc; + if (useFingerLocation && fingerLocation != null) { + myLoc = new Location(""); + myLoc.setLatitude(fingerLocation.getLatitude()); + myLoc.setLongitude(fingerLocation.getLongitude()); + } else { + myLoc = app.getLocationProvider().getLastStaleKnownLocation(); + } + MapMarkersHelper markersHelper = app.getMapMarkersHelper(); + List activeMapMarkers = markersHelper.getMapMarkers(); + int displayedWidgets = settings.DISPLAYED_MARKERS_WIDGETS_COUNT.get(); + + if (route != null && route.points.size() > 0) { + planRouteAttrs.updatePaints(app, nightMode, tileBox); + new Renderable.StandardTrack(new ArrayList<>(route.points), 17.2). + drawSegment(view.getZoom(), defaultAppMode ? planRouteAttrs.paint : planRouteAttrs.paint2, canvas, tileBox); + } + + if (settings.SHOW_LINES_TO_FIRST_MARKERS.get() && myLoc != null) { + textAttrs.paint.setTextSize(textSize); + textAttrs.paint2.setTextSize(textSize); + + lineAttrs.updatePaints(app, nightMode, tileBox); + textAttrs.updatePaints(app, nightMode, tileBox); + textAttrs.paint.setStyle(Paint.Style.FILL); + + textPaint.set(textAttrs.paint); + + boolean drawMarkerName = settings.DISPLAYED_MARKERS_WIDGETS_COUNT.get() == 1; + + float locX; + float locY; + if (app.getMapViewTrackingUtilities().isMapLinkedToLocation() + && !MapViewTrackingUtilities.isSmallSpeedForAnimation(myLoc) + && !app.getMapViewTrackingUtilities().isMovingToMyLocation()) { + locX = tileBox.getPixXFromLatLon(tileBox.getLatitude(), tileBox.getLongitude()); + locY = tileBox.getPixYFromLatLon(tileBox.getLatitude(), tileBox.getLongitude()); + } else { + locX = tileBox.getPixXFromLatLon(myLoc.getLatitude(), myLoc.getLongitude()); + locY = tileBox.getPixYFromLatLon(myLoc.getLatitude(), myLoc.getLongitude()); + } + int[] colors = MapMarker.getColors(application); + for (int i = 0; i < activeMapMarkers.size() && i < displayedWidgets; i++) { + MapMarker marker = activeMapMarkers.get(i); + float markerX = tileBox.getPixXFromLatLon(marker.getLatitude(), marker.getLongitude()); + float markerY = tileBox.getPixYFromLatLon(marker.getLatitude(), marker.getLongitude()); + + linePath.reset(); + tx.clear(); + ty.clear(); + + tx.add(locX); + ty.add(locY); + tx.add(markerX); + ty.add(markerY); + + GeometryWay.calculatePath(tileBox, tx, ty, linePath); + PathMeasure pm = new PathMeasure(linePath, false); + float[] pos = new float[2]; + pm.getPosTan(pm.getLength() / 2, pos, null); + + float dist = (float) MapUtils.getDistance(myLoc.getLatitude(), myLoc.getLongitude(), marker.getLatitude(), marker.getLongitude()); + String distSt = OsmAndFormatter.getFormattedDistance(dist, view.getApplication()); + String text = distSt + (drawMarkerName ? " • " + marker.getName(application) : ""); + text = TextUtils.ellipsize(text, textPaint, pm.getLength(), TextUtils.TruncateAt.END).toString(); + Rect bounds = new Rect(); + textAttrs.paint.getTextBounds(text, 0, text.length(), bounds); + float hOffset = pm.getLength() / 2 - bounds.width() / 2; + lineAttrs.paint.setColor(colors[marker.colorIndex]); + + canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY()); + canvas.drawPath(linePath, lineAttrs.paint); + if (locX >= markerX) { + canvas.rotate(180, pos[0], pos[1]); + canvas.drawTextOnPath(text, linePath, hOffset, bounds.height() + verticalOffset, textAttrs.paint2); + canvas.drawTextOnPath(text, linePath, hOffset, bounds.height() + verticalOffset, textAttrs.paint); + canvas.rotate(-180, pos[0], pos[1]); + } else { + canvas.drawTextOnPath(text, linePath, hOffset, -verticalOffset, textAttrs.paint2); + canvas.drawTextOnPath(text, linePath, hOffset, -verticalOffset, textAttrs.paint); + } + canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY()); + } + } + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings nightMode) { + //widgetsFactory.updateInfo(useFingerLocation ? fingerLocation : null, tileBox.getZoom()); + OsmandSettings settings = application.getSettings(); + + if (tileBox.getZoom() < 3 || !settings.SHOW_MAP_MARKERS.get()) { + return; + } + + int displayedWidgets = settings.DISPLAYED_MARKERS_WIDGETS_COUNT.get(); + + MapMarkersHelper markersHelper = application.getMapMarkersHelper(); + + for (MapMarker marker : markersHelper.getMapMarkers()) { + if (isLocationVisible(tileBox, marker) && !overlappedByWaypoint(marker) + && !isInMotion(marker) && !isSynced(marker)) { + Bitmap bmp = getMapMarkerBitmap(marker.colorIndex); + int marginX = bmp.getWidth() / 6; + int marginY = bmp.getHeight(); + int locationX = tileBox.getPixXFromLonNoRot(marker.getLongitude()); + int locationY = tileBox.getPixYFromLatNoRot(marker.getLatitude()); + canvas.rotate(-tileBox.getRotate(), locationX, locationY); + canvas.drawBitmap(bmp, locationX - marginX, locationY - marginY, bitmapPaint); + canvas.rotate(tileBox.getRotate(), locationX, locationY); + } + } + + if (settings.SHOW_ARROWS_TO_FIRST_MARKERS.get()) { + LatLon loc = tileBox.getCenterLatLon(); + int i = 0; + for (MapMarker marker : markersHelper.getMapMarkers()) { + if (!isLocationVisible(tileBox, marker) && !isInMotion(marker)) { + canvas.save(); + net.osmand.Location.distanceBetween(loc.getLatitude(), loc.getLongitude(), + marker.getLatitude(), marker.getLongitude(), calculations); + float bearing = calculations[1] - 90; + float radiusBearing = DIST_TO_SHOW * tileBox.getDensity(); + final QuadPoint cp = tileBox.getCenterPixelPoint(); + canvas.rotate(bearing, cp.x, cp.y); + canvas.translate(-24 * tileBox.getDensity() + radiusBearing, -22 * tileBox.getDensity()); + canvas.drawBitmap(arrowShadow, cp.x, cp.y, bitmapPaint); + canvas.drawBitmap(arrowToDestination, cp.x, cp.y, getMarkerDestPaint(marker.colorIndex)); + canvas.drawBitmap(arrowLight, cp.x, cp.y, bitmapPaint); + canvas.restore(); + } + i++; + if (i > displayedWidgets - 1) { + break; + } + } + } + +// if (contextMenuLayer.getMoveableObject() instanceof MapMarker) { +// MapMarker objectInMotion = (MapMarker) contextMenuLayer.getMoveableObject(); +// PointF pf = contextMenuLayer.getMovableCenterPoint(tileBox); +// Bitmap bitmap = getMapMarkerBitmap(objectInMotion.colorIndex); +// int marginX = bitmap.getWidth() / 6; +// int marginY = bitmap.getHeight(); +// float locationX = pf.x; +// float locationY = pf.y; +// canvas.rotate(-tileBox.getRotate(), locationX, locationY); +// canvas.drawBitmap(bitmap, locationX - marginX, locationY - marginY, bitmapPaint); +// +// } + } + + private boolean isSynced(@NonNull MapMarker marker) { + return marker.wptPt != null || marker.favouritePoint != null; + } + + private boolean isInMotion(@NonNull MapMarker marker) { + return marker.equals(contextMenuLayer.getMoveableObject()); + } + + public boolean isLocationVisible(RotatedTileBox tb, MapMarker marker) { + //noinspection SimplifiableIfStatement + if (marker == null || tb == null) { + return false; + } + return containsLatLon(tb, marker.getLatitude(), marker.getLongitude()); + } + + public boolean containsLatLon(RotatedTileBox tb, double lat, double lon) { + double widgetHeight = 0; + if (widgetsFactory.isTopBarVisible()) { + widgetHeight = widgetsFactory.getTopBarHeight(); + } + double tx = tb.getPixXFromLatLon(lat, lon); + double ty = tb.getPixYFromLatLon(lat, lon); + return tx >= 0 && tx <= tb.getPixWidth() && ty >= widgetHeight && ty <= tb.getPixHeight(); + } + + public boolean overlappedByWaypoint(MapMarker marker) { + List targetPoints = application.getTargetPointsHelper().getAllPoints(); + for (TargetPoint t : targetPoints) { + if (t.point.equals(marker.point)) { + return true; + } + } + return false; + } + + @Override + public void destroyLayer() { + } + + @Override + public boolean onTouchEvent(MotionEvent event, RotatedTileBox tileBox) { + if (!longTapDetector.onTouchEvent(event)) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + float x = event.getX(); + float y = event.getY(); + fingerLocation = tileBox.getLatLonFromPixel(x, y); + hasMoved = false; + moving = true; + break; + + case MotionEvent.ACTION_MOVE: + if (!hasMoved) { + if (!handler.hasMessages(MAP_REFRESH_MESSAGE)) { + Message msg = Message.obtain(handler, new Runnable() { + @Override + public void run() { + handler.removeMessages(MAP_REFRESH_MESSAGE); + if (moving) { + if (!useFingerLocation) { + useFingerLocation = true; + } + } + } + }); + msg.what = MAP_REFRESH_MESSAGE; + handler.sendMessageDelayed(msg, USE_FINGER_LOCATION_DELAY); + } + hasMoved = true; + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + cancelFingerAction(); + break; + } + } + return super.onTouchEvent(event, tileBox); + } + + private void cancelFingerAction() { + handler.removeMessages(MAP_REFRESH_MESSAGE); + useFingerLocation = false; + moving = false; + fingerLocation = null; + } + + @Override + public boolean drawInScreenPixels() { + return false; + } + + @Override + public boolean disableSingleTap() { + return inPlanRouteMode; + } + + @Override + public boolean disableLongPressOnMap() { + return inPlanRouteMode; + } + + @Override + public boolean isObjectClickable(Object o) { + return false; + } + + @Override + public boolean runExclusiveAction(Object o, boolean unknownLocation) { + OsmandSettings settings = application.getSettings(); + if (unknownLocation + || o == null + || !(o instanceof MapMarker) + || !settings.SELECT_MARKER_ON_SINGLE_TAP.get() + || !settings.SHOW_MAP_MARKERS.get()) { + return false; + } + final MapMarkersHelper helper = application.getMapMarkersHelper(); + final MapMarker old = helper.getMapMarkers().get(0); + helper.moveMarkerToTop((MapMarker) o); + String title = application.getString(R.string.marker_activated, helper.getMapMarkers().get(0).getName(application)); + return true; + } + + @Override + public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List o, boolean unknownLocation) { + if (tileBox.getZoom() < 3 || !application.getSettings().SHOW_MAP_MARKERS.get()) { + return; + } + amenities.clear(); + OsmandApplication app = application; + int r = getDefaultRadiusPoi(tileBox); + boolean selectMarkerOnSingleTap = app.getSettings().SELECT_MARKER_ON_SINGLE_TAP.get(); + + for (MapMarker marker : app.getMapMarkersHelper().getMapMarkers()) { + if ((!unknownLocation && selectMarkerOnSingleTap) || !isSynced(marker)) { + LatLon latLon = marker.point; + if (latLon != null) { + int x = (int) tileBox.getPixXFromLatLon(latLon.getLatitude(), latLon.getLongitude()); + int y = (int) tileBox.getPixYFromLatLon(latLon.getLatitude(), latLon.getLongitude()); + + if (calculateBelongs((int) point.x, (int) point.y, x, y, r)) { + if (!unknownLocation && selectMarkerOnSingleTap) { + o.add(marker); + } else { + if (isMarkerOnFavorite(marker) && app.getSettings().SHOW_FAVORITES.get() + || isMarkerOnWaypoint(marker) && app.getSettings().SHOW_WPT.get()) { + continue; + } + Amenity mapObj = getMapObjectByMarker(marker); + if (mapObj != null) { + amenities.add(mapObj); + o.add(mapObj); + } else { + o.add(marker); + } + } + } + } + } + } + } + + private boolean isMarkerOnWaypoint(@NonNull MapMarker marker) { + return marker.point != null && application.getSelectedGpxHelper().getVisibleWayPointByLatLon(marker.point) != null; + } + + private boolean isMarkerOnFavorite(@NonNull MapMarker marker) { + return marker.point != null && application.getFavorites().getVisibleFavByLatLon(marker.point) != null; + } + + @Nullable + public Amenity getMapObjectByMarker(@NonNull MapMarker marker) { + if (marker.mapObjectName != null && marker.point != null) { + String mapObjName = marker.mapObjectName.split("_")[0]; + return findAmenity(application, -1, Collections.singletonList(mapObjName), marker.point, 15); + } + return null; + } + + private boolean calculateBelongs(int ex, int ey, int objx, int objy, int radius) { + return Math.abs(objx - ex) <= radius * 1.5 && (ey - objy) <= radius * 1.5 && (objy - ey) <= 2.5 * radius; + } + + @Override + public LatLon getObjectLocation(Object o) { + if (o instanceof MapMarker) { + return ((MapMarker) o).point; + } else if (o instanceof Amenity && amenities.contains(o)) { + return ((Amenity) o).getLocation(); + } + return null; + } + + + @Override + public PointDescription getObjectName(Object o) { +// if (o instanceof MapMarker) { +// return ((MapMarker) o).getPointDescription(view.getContext()); +// } + return null; + } + + @Override + public int getOrder(Object o) { + return 0; + } + + @Override + public void setSelectedObject(Object o) { + } + + @Override + public void clearSelectedObject() { + } + + @Override + public boolean isObjectMovable(Object o) { + return o instanceof MapMarker; + } + + @Override + public void applyNewObjectPosition(@NonNull Object o, @NonNull LatLon position, + @Nullable ApplyMovedObjectCallback callback) { + boolean result = false; + MapMarker newObject = null; + if (o instanceof MapMarker) { + MapMarkersHelper markersHelper = application.getMapMarkersHelper(); + MapMarker marker = (MapMarker) o; + + PointDescription originalDescription = marker.getOriginalPointDescription(); + if (originalDescription.isLocation()) { + originalDescription.setName(PointDescription.getSearchAddressStr(application)); + } + markersHelper.moveMapMarker(marker, position); + int index = markersHelper.getMapMarkers().indexOf(marker); + if (index != -1) { + newObject = markersHelper.getMapMarkers().get(index); + } + result = true; + } + if (callback != null) { + callback.onApplyMovedObject(result, newObject == null ? o : newObject); + } + } +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/MapMarkersWidgetsMiniFactory.java b/OsmAnd/src/net/osmand/plus/server/map/MapMarkersWidgetsMiniFactory.java new file mode 100644 index 0000000000..125834a291 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/MapMarkersWidgetsMiniFactory.java @@ -0,0 +1,306 @@ +package net.osmand.plus.server.map; + +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import net.osmand.Location; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.plus.*; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.helpers.AndroidUiHelper; +import net.osmand.plus.mapmarkers.MapMarkersDialogFragment; +import net.osmand.plus.views.AnimateDraggingMapThread; +import net.osmand.plus.views.DirectionDrawable; +import net.osmand.plus.views.OsmandMapLayer; +import net.osmand.plus.views.OsmandMapTileView; +import net.osmand.plus.views.mapwidgets.widgets.TextInfoWidget; +import net.osmand.util.Algorithms; +import net.osmand.util.MapUtils; + +import java.util.List; +public class MapMarkersWidgetsMiniFactory { + + public static final int MIN_DIST_OK_VISIBLE = 40; // meters + public static final int MIN_DIST_2ND_ROW_SHOW = 150; // meters + private static OsmandApplication app; + + private MapMarkersHelper helper; + private int screenOrientation; + private boolean portraitMode; + + + private LatLon loc; + + private boolean cachedTopBarVisibility; + + public MapMarkersWidgetsMiniFactory(final OsmandApplication application) { + this.app = application; + helper = app.getMapMarkersHelper(); + screenOrientation = app.getUIUtilities().getScreenOrientation(); + //portraitMode = AndroidUiHelper.isOrientationPortrait(map); + + updateVisibility(false); + } + + private void removeMarker(int index) { + if (helper.getMapMarkers().size() > index) { + helper.moveMapMarkerToHistory(helper.getMapMarkers().get(index)); + } + } + + private void showMarkerOnMap(int index) { +// if (helper.getMapMarkers().size() > index) { +// MapMarkersHelper.MapMarker marker = helper.getMapMarkers().get(index); +// AnimateDraggingMapThread thread = app.getMapView().getAnimatedDraggingThread(); +// LatLon pointToNavigate = marker.point; +// if (pointToNavigate != null) { +// int fZoom = map.getMapView().getZoom() < 15 ? 15 : map.getMapView().getZoom(); +// thread.startMoving(pointToNavigate.getLatitude(), pointToNavigate.getLongitude(), fZoom, true); +// } +// //MapMarkerDialogHelper.showMarkerOnMap(map, marker); +// } + } + + public boolean updateVisibility(boolean visible) { + return visible; + } + + public int getTopBarHeight() { + return 0; + } + + public boolean isTopBarVisible() { + return false; + } + + public void updateInfo(LatLon customLocation, int zoom) { + if (customLocation != null) { + loc = customLocation; + } else { + Location l = app.getLocationProvider().getLastStaleKnownLocation(); + if (l != null) { + loc = new LatLon(l.getLatitude(), l.getLongitude()); + } else { + } + } + + List markers = helper.getMapMarkers(); + if (zoom < 3 || markers.size() == 0 + || !app.getSettings().MARKERS_DISTANCE_INDICATION_ENABLED.get() + || !app.getSettings().MAP_MARKERS_MODE.get().isToolbar() + || app.getRoutingHelper().isFollowingMode() + || app.getRoutingHelper().isRoutePlanningMode()) { + updateVisibility(false); + return; + } + + Float heading = app.getMapViewTrackingUtilities().getHeading(); + MapMarkersHelper.MapMarker marker = markers.get(0); + + if (markers.size() > 1 && app.getSettings().DISPLAYED_MARKERS_WIDGETS_COUNT.get() == 2) { + marker = markers.get(1); + if (loc != null && customLocation == null) { + for (int i = 1; i < markers.size(); i++) { + MapMarkersHelper.MapMarker m = markers.get(i); + m.dist = (int) (MapUtils.getDistance(m.getLatitude(), m.getLongitude(), + loc.getLatitude(), loc.getLongitude())); + if (m.dist < MIN_DIST_2ND_ROW_SHOW && marker.dist > m.dist) { + marker = m; + } + } + } + } else { + } + + updateVisibility(true); + } + + private void updateUI(LatLon loc, Float heading, MapMarkersHelper.MapMarker marker, ImageView arrowImg, + TextView distText, ImageButton okButton, TextView addressText, + boolean firstLine, boolean customLocation) { + float[] mes = new float[2]; + if (loc != null && marker.point != null) { + Location.distanceBetween(marker.getLatitude(), marker.getLongitude(), loc.getLatitude(), loc.getLongitude(), mes); + } + + if (customLocation) { + heading = 0f; + } + + boolean newImage = false; + DirectionDrawable dd; + if (!(arrowImg.getDrawable() instanceof DirectionDrawable)) { + newImage = true; + dd = new DirectionDrawable(app, arrowImg.getWidth(), arrowImg.getHeight()); + } else { + dd = (DirectionDrawable) arrowImg.getDrawable(); + } + dd.setImage(R.drawable.ic_arrow_marker_diretion, MapMarkersHelper.MapMarker.getColorId(marker.colorIndex)); + if (heading != null && loc != null) { + dd.setAngle(mes[1] - heading + 180 + screenOrientation); + } + if (newImage) { + arrowImg.setImageDrawable(dd); + } + arrowImg.invalidate(); + + int dist = (int) mes[0]; + String txt; + if (loc != null) { + txt = OsmAndFormatter.getFormattedDistance(dist, app); + } else { + txt = "—"; + } + if (txt != null) { + distText.setText(txt); + } + AndroidUiHelper.updateVisibility(okButton, !customLocation && loc != null && dist < MIN_DIST_OK_VISIBLE); + + String descr; + PointDescription pd = marker.getPointDescription(app); + if (Algorithms.isEmpty(pd.getName())) { + descr = pd.getTypeName(); + } else { + descr = pd.getName(); + } + if (!firstLine && !isLandscapeLayout()) { + descr = " • " + descr; + } + + addressText.setText(descr); + } + + public TextInfoWidget createMapMarkerControl(final MapActivity map, final boolean firstMarker) { + return new net.osmand.plus.views.mapwidgets.MapMarkersWidgetsFactory.DistanceToMapMarkerControl(map, firstMarker) { + @Override + public LatLon getLatLon() { + return loc; + } + + @Override + protected void click(OsmandMapTileView view) { + showMarkerOnMap(firstMarker ? 0 : 1); + } + }; + } + + public boolean isLandscapeLayout() { + return !portraitMode; + } + + public abstract static class DistanceToMapMarkerControl extends TextInfoWidget { + + private boolean firstMarker; + private final OsmandMapTileView view; + private MapActivity map; + private MapMarkersHelper helper; + private float[] calculations = new float[1]; + private int cachedMeters; + private int cachedMarkerColorIndex = -1; + private Boolean cachedNightMode = null; + + public DistanceToMapMarkerControl(MapActivity map, boolean firstMarker) { + super(map); + this.map = map; + this.firstMarker = firstMarker; + this.view = map.getMapView(); + helper = app.getMapMarkersHelper(); + setText(null, null); + setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + click(view); + } + }); + } + + protected abstract void click(OsmandMapTileView view); + + public abstract LatLon getLatLon(); + + @Override + public boolean updateInfo(OsmandMapLayer.DrawSettings drawSettings) { + MapMarkersHelper.MapMarker marker = getMarker(); + if (marker == null + || app.getRoutingHelper().isRoutePlanningMode() + || app.getRoutingHelper().isFollowingMode()) { + cachedMeters = 0; + setText(null, null); + return false; + } + boolean res = false; + int d = getDistance(); + if (isUpdateNeeded() || cachedMeters != d) { + cachedMeters = d; + String ds = OsmAndFormatter.getFormattedDistance(cachedMeters, app); + int ls = ds.lastIndexOf(' '); + if (ls == -1) { + setText(ds, null); + } else { + setText(ds.substring(0, ls), ds.substring(ls + 1)); + } + res = true; + } + + if (marker.colorIndex != -1) { + if (marker.colorIndex != cachedMarkerColorIndex + || cachedNightMode == null || cachedNightMode != isNight()) { + setImageDrawable(app.getUIUtilities() + .getLayeredIcon(isNight() ? R.drawable.widget_marker_night : R.drawable.widget_marker_day, + R.drawable.widget_marker_triangle, 0, + MapMarkersHelper.MapMarker.getColorId(marker.colorIndex))); + cachedMarkerColorIndex = marker.colorIndex; + cachedNightMode = isNight(); + res = true; + } + } + return res; + } + + @Override + public boolean isMetricSystemDepended() { + return true; + } + + public LatLon getPointToNavigate() { + MapMarkersHelper.MapMarker marker = getMarker(); + if (marker != null) { + return marker.point; + } + return null; + } + + private MapMarkersHelper.MapMarker getMarker() { + List markers = helper.getMapMarkers(); + if (firstMarker) { + if (markers.size() > 0) { + return markers.get(0); + } + } else { + if (markers.size() > 1) { + return markers.get(1); + } + } + return null; + } + + public int getDistance() { + int d = 0; + LatLon l = getPointToNavigate(); + if (l != null) { + LatLon loc = getLatLon(); + if (loc == null) { + Location.distanceBetween(view.getLatitude(), view.getLongitude(), l.getLatitude(), l.getLongitude(), calculations); + } else { + Location.distanceBetween(loc.getLatitude(), loc.getLongitude(), l.getLatitude(), l.getLongitude(), calculations); + } + d = (int) calculations[0]; + } + return d; + } + } +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/MapTextMiniLayer.java b/OsmAnd/src/net/osmand/plus/server/map/MapTextMiniLayer.java new file mode 100644 index 0000000000..67f205ad3e --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/MapTextMiniLayer.java @@ -0,0 +1,223 @@ +package net.osmand.plus.server.map; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.TextUtils; +import gnu.trove.set.hash.TIntHashSet; +import net.osmand.data.LatLon; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.R; +import net.osmand.plus.views.OsmandMapLayer; +import net.osmand.plus.views.OsmandMapTileView; + +import java.util.*; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.text.TextUtils; + +import net.osmand.data.LatLon; +import net.osmand.data.RotatedTileBox; + +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +import gnu.trove.set.hash.TIntHashSet; +import net.osmand.plus.R; +import net.osmand.plus.views.OsmandMapLayer; +import net.osmand.plus.views.OsmandMapTileView; + +public class MapTextMiniLayer extends OsmandMapLayer { + + private static final int TEXT_WRAP = 15; + private static final int TEXT_LINES = 3; + private static final int TEXT_SIZE = 13; + + private Map> textObjects = new LinkedHashMap<>(); + private Paint paintTextIcon; + private OsmandMapTileView view; + + public interface MapTextProvider { + + LatLon getTextLocation(T o); + + int getTextShift(T o, RotatedTileBox rb); + + String getText(T o); + + boolean isTextVisible(); + + boolean isFakeBoldText(); + } + + public void putData(OsmandMapLayer ml, Collection objects) { + if (objects == null || objects.isEmpty()) { + textObjects.remove(ml); + } else { + if (ml instanceof net.osmand.plus.views.layers.MapTextLayer.MapTextProvider) { + textObjects.put(ml, objects); + } else { + throw new IllegalArgumentException(); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + TIntHashSet set = new TIntHashSet(); + for (OsmandMapLayer l : textObjects.keySet()) { + net.osmand.plus.views.layers.MapTextLayer.MapTextProvider provider = (net.osmand.plus.views.layers.MapTextLayer.MapTextProvider) l; + if (!view.isLayerVisible(l) || !provider.isTextVisible()) { + continue; + } + + updateTextSize(); + paintTextIcon.setFakeBoldText(provider.isFakeBoldText()); + for (Object o : textObjects.get(l)) { + LatLon loc = provider.getTextLocation(o); + String name = provider.getText(o); + if (loc == null || TextUtils.isEmpty(name)) { + continue; + } + + int x = (int) tileBox.getPixXFromLatLon(loc.getLatitude(), loc.getLongitude()); + int y = (int) tileBox.getPixYFromLatLon(loc.getLatitude(), loc.getLongitude()); + int tx = tileBox.getPixXFromLonNoRot(loc.getLongitude()); + int ty = tileBox.getPixYFromLatNoRot(loc.getLatitude()); + + int lines = 0; + while (lines < TEXT_LINES) { + if (set.contains(division(tx, ty, 0, lines)) || set.contains(division(tx, ty, -1, lines)) + || set.contains(division(tx, ty, +1, lines))) { + break; + } + lines++; + } + if (lines != 0) { + int r = provider.getTextShift(o, tileBox); + drawWrappedText(canvas, name, paintTextIcon.getTextSize(), x, + y + r + 2 + paintTextIcon.getTextSize() / 2, lines); + while (lines > 0) { + set.add(division(tx, ty, 1, lines - 1)); + set.add(division(tx, ty, -1, lines - 1)); + set.add(division(tx, ty, 0, lines - 1)); + lines--; + } + } + } + } + } + + private int division(int x, int y, int sx, int sy) { + // make numbers positive + return ((((x + 10000) >> 4) + sx) << 16) | (((y + 10000) >> 4) + sy); + } + + private void drawWrappedText(Canvas cv, String text, float textSize, float x, float y, int lines) { + boolean nightMode = view.getApplication().getDaynightHelper().isNightMode(); + if (text.length() > TEXT_WRAP) { + int start = 0; + int end = text.length(); + int lastSpace = -1; + int line = 0; + int pos = 0; + int limit = 0; + while (pos < end && (line < lines)) { + lastSpace = -1; + limit += TEXT_WRAP; + while (pos < limit && pos < end) { + if (Character.isWhitespace(text.charAt(pos))) { + lastSpace = pos; + } + pos++; + } + if (lastSpace == -1 || (pos == end)) { + drawShadowText(cv, text.substring(start, pos), x, y + line * (textSize + 2), nightMode); + start = pos; + } else { + String subtext = text.substring(start, lastSpace); + if (line + 1 == lines) { + subtext += ".."; + } + drawShadowText(cv, subtext, x, y + line * (textSize + 2), nightMode); + + start = lastSpace + 1; + limit += (start - pos) - 1; + } + + line++; + } + } else { + drawShadowText(cv, text, x, y, nightMode); + } + } + + private void drawShadowText(Canvas cv, String text, float centerX, float centerY, boolean nightMode) { + Resources r = view.getApplication().getResources(); + paintTextIcon.setStyle(Paint.Style.STROKE); + paintTextIcon.setColor(nightMode + ? r.getColor(R.color.widgettext_shadow_night) + : r.getColor(R.color.widgettext_shadow_day)); + paintTextIcon.setStrokeWidth(2); + cv.drawText(text, centerX, centerY, paintTextIcon); + // reset + paintTextIcon.setStrokeWidth(2); + paintTextIcon.setStyle(Paint.Style.FILL); + paintTextIcon.setColor(nightMode + ? r.getColor(R.color.widgettext_night) + : r.getColor(R.color.widgettext_day)); + cv.drawText(text, centerX, centerY, paintTextIcon); + } + + @Override + public void initLayer(OsmandMapTileView v) { + this.view = v; + paintTextIcon = new Paint(); + updateTextSize(); + paintTextIcon.setTextAlign(Paint.Align.CENTER); + paintTextIcon.setAntiAlias(true); + Map> textObjectsLoc = new TreeMap<>(new Comparator() { + @Override + public int compare(OsmandMapLayer lhs, OsmandMapLayer rhs) { + if (view != null) { + float z1 = view.getZorder(lhs); + float z2 = view.getZorder(rhs); + return Float.compare(z1, z2); + } + return 0; + } + }); + textObjectsLoc.putAll(this.textObjects); + this.textObjects = textObjectsLoc; + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + } + + @Override + public void destroyLayer() { + } + + @Override + public boolean drawInScreenPixels() { + return true; + } + + private void updateTextSize() { + float scale = view.getApplication().getSettings().TEXT_SCALE.get(); + float textSize = scale * TEXT_SIZE * view.getDensity(); + if (paintTextIcon.getTextSize() != textSize) { + paintTextIcon.setTextSize(textSize); + } + } +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/MapTileMiniLayer.java b/OsmAnd/src/net/osmand/plus/server/map/MapTileMiniLayer.java new file mode 100644 index 0000000000..588e91ab8e --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/MapTileMiniLayer.java @@ -0,0 +1,276 @@ +package net.osmand.plus.server.map; + + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.widget.Toast; + +import net.osmand.data.QuadRect; +import net.osmand.data.RotatedTileBox; +import net.osmand.map.ITileSource; +import net.osmand.map.TileSourceManager; +import net.osmand.map.TileSourceManager.TileSourceTemplate; +import net.osmand.plus.OsmandPlugin; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.R; +import net.osmand.plus.mapillary.MapillaryPlugin; +import net.osmand.plus.rastermaps.OsmandRasterMapsPlugin; +import net.osmand.plus.resources.ResourceManager; +import net.osmand.plus.views.BaseMapLayer; +import net.osmand.plus.views.MapTileAdapter; +import net.osmand.plus.views.OsmandMapTileView; +import net.osmand.plus.views.YandexTrafficAdapter; +import net.osmand.util.MapUtils; + +public class MapTileMiniLayer extends OsmandMapMiniLayer { + + protected static final int emptyTileDivisor = 16; + public static final int OVERZOOM_IN = 2; + + protected final boolean mainMap; + protected ITileSource map = null; + + protected Paint paintBitmap; + protected RectF bitmapToDraw = new RectF(); + protected Rect bitmapToZoom = new Rect(); + + + protected ResourceManager resourceManager; + protected OsmandSettings settings; + private boolean visible = true; + private boolean useSampling; + + + public MapTileMiniLayer(boolean mainMap) { + this.mainMap = mainMap; + } + + @Override + public boolean drawInScreenPixels() { + return false; + } + + @Override + public void initLayer(OsmandMapTileMiniView view) { + settings = view.getSettings(); + resourceManager = view.getApplication().getResourceManager(); + + useSampling = Build.VERSION.SDK_INT < 28; + + paintBitmap = new Paint(); + paintBitmap.setFilterBitmap(true); + + + } + + public void setAlpha(int alpha) { + if (paintBitmap != null) { + paintBitmap.setAlpha(alpha); + } + } + + public void setMapTileAdapter(MapTileAdapter mapTileAdapter) { + + } + + public void setMapForMapTileAdapter(ITileSource map, MapTileAdapter mapTileAdapter) { + + } + + public void setMap(ITileSource map) { + MapTileAdapter target = null; + if (map instanceof TileSourceTemplate) { + if (TileSourceManager.RULE_YANDEX_TRAFFIC.equals(((TileSourceTemplate) map).getRule())) { + map = null; + target = new YandexTrafficAdapter(); + } + + } + this.map = map; + setMapTileAdapter(target); + } + + public MapTileAdapter getMapTileAdapter() { + return null; + } + + @SuppressLint("WrongCall") + @Override + public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, + DrawSettings drawSettings) { + + drawTileMap(canvas, tileBox); + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings drawSettings) { + + } + + public void drawTileMap(Canvas canvas, RotatedTileBox tileBox) { + ITileSource map = this.map; + if (map == null) { + return; + } + ResourceManager mgr = resourceManager; + int nzoom = tileBox.getZoom(); + final QuadRect tilesRect = tileBox.getTileBounds(); + + // recalculate for ellipsoid coordinates + float ellipticTileCorrection = 0; + if (map.isEllipticYTile()) { + ellipticTileCorrection = (float) (MapUtils.getTileEllipsoidNumberY(nzoom, tileBox.getLatitude()) - tileBox.getCenterTileY()); + } + + + int left = (int) Math.floor(tilesRect.left); + int top = (int) Math.floor(tilesRect.top + ellipticTileCorrection); + int width = (int) Math.ceil(tilesRect.right - left); + int height = (int) Math.ceil(tilesRect.bottom + ellipticTileCorrection - top); + + boolean useInternet = (OsmandPlugin.getEnabledPlugin(OsmandRasterMapsPlugin.class) != null || OsmandPlugin.getEnabledPlugin(MapillaryPlugin.class) != null) + && settings.isInternetConnectionAvailable() && map.couldBeDownloadedFromInternet(); + int maxLevel = map.getMaximumZoomSupported(); + int tileSize = map.getTileSize(); + boolean oneTileShown = false; + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + int leftPlusI = left + i; + int topPlusJ = top + j; + + int x1 = tileBox.getPixXFromTileXNoRot(leftPlusI); + int x2 = tileBox.getPixXFromTileXNoRot(leftPlusI + 1); + + int y1 = tileBox.getPixYFromTileYNoRot(topPlusJ - ellipticTileCorrection); + int y2 = tileBox.getPixYFromTileYNoRot(topPlusJ + 1 - ellipticTileCorrection); + bitmapToDraw.set(x1, y1, x2 , y2); + + final int tileX = leftPlusI; + final int tileY = topPlusJ; + Bitmap bmp = null; + String ordImgTile = mgr.calculateTileId(map, tileX, tileY, nzoom); + // asking tile image async + boolean imgExist = mgr.tileExistOnFileSystem(ordImgTile, map, tileX, tileY, nzoom); + boolean originalWillBeLoaded = useInternet && nzoom <= maxLevel; + if (imgExist || originalWillBeLoaded) { + bmp = mgr.getBitmapTilesCache().getTileForMapAsync(ordImgTile, map, tileX, tileY, nzoom, useInternet); + } + if (bmp == null) { + int div = 1; + boolean readFromCache = originalWillBeLoaded || imgExist; + boolean loadIfExists = !readFromCache; + // asking if there is small version of the map (in cache) + int allowedScale = Math.min(OVERZOOM_IN + Math.max(0, nzoom - map.getMaximumZoomSupported()), 8); + int kzoom = 1; + for (; kzoom <= allowedScale; kzoom++) { + div *= 2; + String imgTileId = mgr.calculateTileId(map, tileX / div, tileY / div, nzoom - kzoom); + if (readFromCache) { + bmp = mgr.getBitmapTilesCache().get(imgTileId); + if (bmp != null) { + break; + } + } else if (loadIfExists) { + if (mgr.tileExistOnFileSystem(imgTileId, map, tileX / div, tileY / div, nzoom - kzoom) + || (useInternet && nzoom - kzoom <= maxLevel)) { + bmp = mgr.getBitmapTilesCache().getTileForMapAsync(imgTileId, map, tileX / div, tileY / div, nzoom + - kzoom, useInternet); + break; + } + } + + } + if (bmp != null) { + if (bmp.getWidth() != tileSize && bmp.getWidth() > 0) { + tileSize = bmp.getWidth(); + } + int xZoom = (tileX % div) * tileSize / div; + int yZoom = (tileY % div) * tileSize / div; + // nice scale + boolean useSampling = this.useSampling && kzoom > 3; + bitmapToZoom.set(Math.max(xZoom, 0), Math.max(yZoom, 0), + Math.min(xZoom + tileSize / div, tileSize), + Math.min(yZoom + tileSize / div, tileSize)); + if (!useSampling) { + canvas.drawBitmap(bmp, bitmapToZoom, bitmapToDraw, paintBitmap); + } else { + int margin = 1; + int scaledSize = tileSize / div; + float innerMargin = 0.5f; + RectF src = new RectF(0, 0, scaledSize, scaledSize); + if (bitmapToZoom.left >= margin) { + bitmapToZoom.left -= margin; + src.left = innerMargin; + src.right += margin; + } + if (bitmapToZoom.top >= margin) { + bitmapToZoom.top -= margin; + src.top = innerMargin; + src.bottom += margin; + } + if (bitmapToZoom.right + margin <= tileSize) { + bitmapToZoom.right += margin; + src.right += margin - innerMargin; + } + if (bitmapToZoom.bottom + margin <= tileSize) { + bitmapToZoom.bottom += margin; + src.bottom += margin - innerMargin; + } + Matrix m = new Matrix(); + RectF dest = new RectF(0, 0, tileSize, tileSize); + m.setRectToRect(src, dest, Matrix.ScaleToFit.FILL); + Bitmap sampled = Bitmap.createBitmap(bmp, + bitmapToZoom.left, bitmapToZoom.top, + bitmapToZoom.width(), bitmapToZoom.height(), m, true); + bitmapToZoom.set(0, 0, tileSize, tileSize); + // very expensive that's why put in the cache + mgr.getBitmapTilesCache().put(ordImgTile, sampled); + canvas.drawBitmap(sampled, bitmapToZoom, bitmapToDraw, paintBitmap); + } + } + } else { + bitmapToZoom.set(0, 0, tileSize, tileSize); + canvas.drawBitmap(bmp, bitmapToZoom, bitmapToDraw, paintBitmap); + } + if (bmp != null) { + oneTileShown = true; + } + } + } + } + + + public int getMaximumShownMapZoom() { + return map == null ? 20 : map.getMaximumZoomSupported() + OVERZOOM_IN; + } + + public int getMinimumShownMapZoom() { + return map == null ? 1 : map.getMinimumZoomSupported(); + } + + @Override + public void destroyLayer() { + setMapTileAdapter(null); + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public ITileSource getMap() { + return map; + } + +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/MapVectorMiniLayer.java b/OsmAnd/src/net/osmand/plus/server/map/MapVectorMiniLayer.java new file mode 100644 index 0000000000..5837ced8be --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/MapVectorMiniLayer.java @@ -0,0 +1,233 @@ +package net.osmand.plus.server.map; + +import android.graphics.*; +import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.MapLayerConfiguration; +import net.osmand.core.jni.PointI; +import net.osmand.data.LatLon; +import net.osmand.data.QuadPointDouble; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.render.MapRenderRepositories; +import net.osmand.plus.resources.ResourceManager; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.views.*; +import net.osmand.plus.views.corenative.NativeCoreContext; +import net.osmand.util.Algorithms; +import net.osmand.util.MapUtils; + + import net.osmand.core.android.MapRendererView; + import net.osmand.core.android.TileSourceProxyProvider; + import net.osmand.core.jni.MapLayerConfiguration; + import net.osmand.core.jni.PointI; + import net.osmand.data.LatLon; + import net.osmand.data.QuadPointDouble; + import net.osmand.data.RotatedTileBox; + import net.osmand.map.ITileSource; + import net.osmand.plus.settings.backend.OsmandSettings; + import net.osmand.plus.resources.ResourceManager; + import net.osmand.plus.views.BaseMapLayer; + import net.osmand.plus.views.MapTileLayer; + import net.osmand.plus.views.OsmandMapTileView; + import net.osmand.plus.views.corenative.NativeCoreContext; + import net.osmand.util.Algorithms; + import net.osmand.util.MapUtils; + import android.graphics.Bitmap; + import android.graphics.Canvas; + import android.graphics.Paint; + import android.graphics.PointF; + import android.graphics.RectF; + +public class MapVectorMiniLayer extends OsmandMapMiniLayer { + + public static final int DEFAULT_MAX_ZOOM = 21; + public static final int DEFAULT_MIN_ZOOM = 1; + private int alpha = 255; + protected int warningToSwitchMapShown = 0; + + public int getAlpha() { + return alpha; + } + + private OsmandMapTileMiniView view; + private ResourceManager resourceManager; + private Paint paintImg; + + private RectF destImage = new RectF(); + private final MapTileMiniLayer tileLayer; + private boolean visible = false; + private boolean oldRender = false; + private String cachedUnderlay; + private Integer cachedMapTransparency; + private String cachedOverlay; + private Integer cachedOverlayTransparency; + + public MapVectorMiniLayer(MapTileMiniLayer tileLayer, boolean oldRender) { + this.tileLayer = tileLayer; + this.oldRender = oldRender; + } + + @Override + public void destroyLayer() { + } + + @Override + public boolean drawInScreenPixels() { + return false; + } + + @Override + public void initLayer(OsmandMapTileMiniView view) { + this.view = view; + //resourceManager = view.getApplication().getResourceManager(); + resourceManager = new ResourceManager(view.getApplication()); + paintImg = new Paint(); + paintImg.setFilterBitmap(true); + paintImg.setAlpha(getAlpha()); + } + + public boolean isVectorDataVisible() { + return visible && view.getZoom() >= view.getSettings().LEVEL_TO_SWITCH_VECTOR_RASTER.get(); + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + if (!visible) { + resourceManager.getRenderer().clearCache(); + } + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tilesRect, DrawSettings drawSettings) { + + } + + @Override + public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tilesRect, DrawSettings drawSettings) { +// if (!visible) { +// return; +// } + // if (!isVectorDataVisible() && tileLayer != null) { + // tileLayer.drawTileMap(canvas, tilesRect); + // resourceManager.getRenderer().interruptLoadingMap(); + // } else { + final MapRendererView mapRenderer = view.getMapRenderer(); + if (mapRenderer != null && !oldRender) { + NativeCoreContext.getMapRendererContext().setNightMode(drawSettings.isNightMode()); + OsmandSettings st = view.getApplication().getSettings(); + /* TODO: Commented to avoid crash (looks like IMapTiledDataProvider.Request parameter does not pass correctly or cannot be resolved while calling obtainImage method) + if (!Algorithms.objectEquals(st.MAP_UNDERLAY.get(), cachedUnderlay)) { + cachedUnderlay = st.MAP_UNDERLAY.get(); + ITileSource tileSource = st.getTileSourceByName(cachedUnderlay, false); + if (tileSource != null) { + TileSourceProxyProvider prov = new TileSourceProxyProvider(view.getApplication(), tileSource); + mapRenderer.setMapLayerProvider(-1, prov.instantiateProxy(true)); + prov.swigReleaseOwnership(); + // mapRenderer.setMapLayerProvider(-1, + // net.osmand.core.jni.OnlineTileSources.getBuiltIn().createProviderFor("Mapnik (OsmAnd)")); + } else { + mapRenderer.resetMapLayerProvider(-1); + } + } + */ + if (!Algorithms.objectEquals(st.MAP_TRANSPARENCY.get(), cachedMapTransparency)) { + cachedMapTransparency = st.MAP_TRANSPARENCY.get(); + MapLayerConfiguration mapLayerConfiguration = new MapLayerConfiguration(); + mapLayerConfiguration.setOpacityFactor(((float) cachedMapTransparency) / 255.0f); + mapRenderer.setMapLayerConfiguration(0, mapLayerConfiguration); + } + /* TODO: Commented to avoid crash (looks like IMapTiledDataProvider.Request parameter does not pass correctly or cannot be resolved while calling obtainImage method) + if (!Algorithms.objectEquals(st.MAP_OVERLAY.get(), cachedOverlay)) { + cachedOverlay = st.MAP_OVERLAY.get(); + ITileSource tileSource = st.getTileSourceByName(cachedOverlay, false); + if (tileSource != null) { + TileSourceProxyProvider prov = new TileSourceProxyProvider(view.getApplication(), tileSource); + mapRenderer.setMapLayerProvider(1, prov.instantiateProxy(true)); + prov.swigReleaseOwnership(); + // mapRenderer.setMapLayerProvider(1, + // net.osmand.core.jni.OnlineTileSources.getBuiltIn().createProviderFor("Mapnik (OsmAnd)")); + } else { + mapRenderer.resetMapLayerProvider(1); + } + } + if (!Algorithms.objectEquals(st.MAP_OVERLAY_TRANSPARENCY.get(), cachedOverlayTransparency)) { + cachedOverlayTransparency = st.MAP_OVERLAY_TRANSPARENCY.get(); + MapLayerConfiguration mapLayerConfiguration = new MapLayerConfiguration(); + mapLayerConfiguration.setOpacityFactor(((float) cachedOverlayTransparency) / 255.0f); + mapRenderer.setMapLayerConfiguration(1, mapLayerConfiguration); + } + */ + // opengl renderer + LatLon ll = tilesRect.getLatLonFromPixel(tilesRect.getPixWidth() / 2, tilesRect.getPixHeight() / 2); + mapRenderer.setTarget(new PointI(MapUtils.get31TileNumberX(ll.getLongitude()), MapUtils.get31TileNumberY(ll + .getLatitude()))); + mapRenderer.setAzimuth(-tilesRect.getRotate()); + mapRenderer.setZoom((float) (tilesRect.getZoom() + tilesRect.getZoomAnimation() + tilesRect + .getZoomFloatPart())); + float zoomMagnifier = st.MAP_DENSITY.get(); + mapRenderer.setVisualZoomShift(zoomMagnifier - 1.0f); + } else { + //if (!view.isZooming()) { + final OsmandMapLayer.DrawSettings drawSettings1 = + new OsmandMapLayer.DrawSettings(drawSettings.isNightMode(), true); + if (resourceManager.updateRenderedMapNeeded(tilesRect, drawSettings1)) { +// pixRect.set(-view.getWidth(), -view.getHeight() / 2, 2 * view.getWidth(), 3 * +// view.getHeight() / 2); + final RotatedTileBox copy = tilesRect.copy(); + copy.increasePixelDimensions(copy.getPixWidth() / 3, copy.getPixHeight() / 4); + resourceManager.updateRendererMap(copy, null); + } + + MapRenderRepositories renderer = resourceManager.getRenderer(); + + RotatedTileBox currentTileBlock = tilesRect; + resourceManager.getRenderer().loadMap(currentTileBlock, resourceManager.getMapTileDownloader()); + drawRenderedMap(canvas, renderer.getBitmap(), renderer.getBitmapLocation(), tilesRect); + drawRenderedMap(canvas, renderer.getPrevBitmap(), renderer.getPrevBmpLocation(), tilesRect); + } + } + + private boolean drawRenderedMap(Canvas canvas, Bitmap bmp, RotatedTileBox bmpLoc, RotatedTileBox currentViewport) { + boolean shown = false; + if (bmp != null && bmpLoc != null) { + float rot = -bmpLoc.getRotate(); + canvas.rotate(rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); + final RotatedTileBox calc = currentViewport.copy(); + calc.setRotate(bmpLoc.getRotate()); + QuadPointDouble lt = bmpLoc.getLeftTopTile(bmpLoc.getZoom()); + QuadPointDouble rb = bmpLoc.getRightBottomTile(bmpLoc.getZoom()); + final float x1 = calc.getPixXFromTile(lt.x, lt.y, bmpLoc.getZoom()); + final float x2 = calc.getPixXFromTile(rb.x, rb.y, bmpLoc.getZoom()); + final float y1 = calc.getPixYFromTile(lt.x, lt.y, bmpLoc.getZoom()); + final float y2 = calc.getPixYFromTile(rb.x, rb.y, bmpLoc.getZoom()); + +// LatLon lt = bmpLoc.getLeftTopLatLon(); +// LatLon rb = bmpLoc.getRightBottomLatLon(); +// final float x1 = calc.getPixXFromLatLon(lt.getLatitude(), lt.getLongitude()); +// final float x2 = calc.getPixXFromLatLon(rb.getLatitude(), rb.getLongitude()); +// final float y1 = calc.getPixYFromLatLon(lt.getLatitude(), lt.getLongitude()); +// final float y2 = calc.getPixYFromLatLon(rb.getLatitude(), rb.getLongitude()); + destImage.set(0, 0, 512, 512); + if (!bmp.isRecycled()) { + canvas.drawBitmap(bmp, null, destImage, paintImg); + shown = true; + } + canvas.rotate(-rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); + } + return shown; + } + + @Override + public boolean onLongPressEvent(PointF point, RotatedTileBox tileBox) { + return false; + } + + @Override + public boolean onSingleTap(PointF point, RotatedTileBox tileBox) { + return false; + } +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/MyCustomLayer.java b/OsmAnd/src/net/osmand/plus/server/map/MyCustomLayer.java new file mode 100644 index 0000000000..a63a51f52f --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/MyCustomLayer.java @@ -0,0 +1,35 @@ +package net.osmand.plus.server.map; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import net.osmand.data.RotatedTileBox; + +public class MyCustomLayer extends OsmandMapMiniLayer{ + + protected Paint paintBitmap; + + + @Override + public void initLayer(OsmandMapTileMiniView view) { + + paintBitmap = new Paint(); + paintBitmap.setFilterBitmap(true); + paintBitmap.setColor(Color.BLACK); + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + //canvas.drawCircle(canvas.getWidth()/2,canvas.getHeight()/2,40,paintBitmap); + } + + @Override + public void destroyLayer() { + + } + + @Override + public boolean drawInScreenPixels() { + return false; + } +} diff --git a/OsmAnd/src/net/osmand/plus/server/map/OsmandMapMiniLayer.java b/OsmAnd/src/net/osmand/plus/server/map/OsmandMapMiniLayer.java new file mode 100644 index 0000000000..ff01736162 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/OsmandMapMiniLayer.java @@ -0,0 +1,482 @@ +package net.osmand.plus.server.map; + +import android.content.Context; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.view.MotionEvent; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import net.osmand.binary.BinaryMapIndexReader; +import net.osmand.data.*; +import net.osmand.osm.PoiCategory; +import net.osmand.plus.ContextMenuAdapter; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.render.OsmandRenderer; +import net.osmand.plus.views.OsmandMapTileView; +import net.osmand.render.RenderingRuleSearchRequest; +import net.osmand.render.RenderingRulesStorage; +import net.osmand.util.MapUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public abstract class OsmandMapMiniLayer { + + protected List fullObjectsLatLon; + protected List smallObjectsLatLon; + + public enum MapGestureType { + DOUBLE_TAP_ZOOM_IN, + DOUBLE_TAP_ZOOM_CHANGE, + TWO_POINTERS_ZOOM_OUT + } + + public boolean isMapGestureAllowed(MapGestureType type) { + return true; + } + + public abstract void initLayer(OsmandMapTileMiniView view); + + public abstract void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings); + + public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + } + + public abstract void destroyLayer(); + + public void onRetainNonConfigurationInstance(Map map) { + } + + public void populateObjectContextMenu(LatLon latLon, Object o, ContextMenuAdapter adapter, MapActivity mapActivity) { + } + + public boolean onSingleTap(PointF point, RotatedTileBox tileBox) { + return false; + } + + public boolean onLongPressEvent(PointF point, RotatedTileBox tileBox) { + return false; + } + + public boolean onTouchEvent(MotionEvent event, RotatedTileBox tileBox) { + return false; + } + + + public void executeTaskInBackground(AsyncTask task, Params... params) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + } + + public boolean isPresentInFullObjects(LatLon latLon) { + if (fullObjectsLatLon == null) { + return true; + } else if (latLon != null) { + return fullObjectsLatLon.contains(latLon); + } + return false; + } + + public boolean isPresentInSmallObjects(LatLon latLon) { + if (smallObjectsLatLon != null && latLon != null) { + return smallObjectsLatLon.contains(latLon); + } + return false; + } + + /** + * This method returns whether canvas should be rotated as + * map rotated before {@link #onDraw(android.graphics.Canvas, net.osmand.data.RotatedTileBox, DrawSettings)}. + * If the layer draws simply layer over screen (not over map) + * it should return true. + */ + public abstract boolean drawInScreenPixels(); + + public static class DrawSettings { + private final boolean nightMode; + private final boolean updateVectorRendering; + + public DrawSettings(boolean nightMode) { + this(nightMode, false); + } + + public DrawSettings(boolean nightMode, boolean updateVectorRendering) { + this.nightMode = nightMode; + this.updateVectorRendering = updateVectorRendering; + } + + public boolean isUpdateVectorRendering() { + return updateVectorRendering; + } + + public boolean isNightMode() { + return nightMode; + } + } + + @NonNull + public static QuadTree initBoundIntersections(RotatedTileBox tileBox) { + QuadRect bounds = new QuadRect(0, 0, tileBox.getPixWidth(), tileBox.getPixHeight()); + bounds.inset(-bounds.width() / 4, -bounds.height() / 4); + return new QuadTree<>(bounds, 4, 0.6f); + } + + public static boolean intersects(QuadTree boundIntersections, float x, float y, float width, float height) { + List result = new ArrayList<>(); + QuadRect visibleRect = calculateRect(x, y, width, height); + boundIntersections.queryInBox(new QuadRect(visibleRect.left, visibleRect.top, visibleRect.right, visibleRect.bottom), result); + for (QuadRect r : result) { + if (QuadRect.intersects(r, visibleRect)) { + return true; + } + } + boundIntersections.insert(visibleRect, + new QuadRect(visibleRect.left, visibleRect.top, visibleRect.right, visibleRect.bottom)); + return false; + } + + public QuadRect getCorrectedQuadRect(QuadRect latlonRect) { + double topLatitude = latlonRect.top; + double leftLongitude = latlonRect.left; + double bottomLatitude = latlonRect.bottom; + double rightLongitude = latlonRect.right; + // double lat = 0; + // double lon = 0; + // this is buggy lat/lon should be 0 but in that case + // it needs to be fixed in case there is no route points in the view bbox + double lat = topLatitude - bottomLatitude + 0.1; + double lon = rightLongitude - leftLongitude + 0.1; + return new QuadRect(leftLongitude - lon, topLatitude + lat, rightLongitude + lon, bottomLatitude - lat); + } + + public static QuadRect calculateRect(float x, float y, float width, float height) { + QuadRect rf; + double left = x - width / 2.0d; + double top = y - height / 2.0d; + double right = left + width; + double bottom = top + height; + rf = new QuadRect(left, top, right, bottom); + return rf; + } + + public Amenity findAmenity(OsmandApplication app, long id, List names, LatLon latLon, int radius) { + QuadRect rect = MapUtils.calculateLatLonBbox(latLon.getLatitude(), latLon.getLongitude(), radius); + List amenities = app.getResourceManager().searchAmenities( + new BinaryMapIndexReader.SearchPoiTypeFilter() { + @Override + public boolean accept(PoiCategory type, String subcategory) { + return true; + } + + @Override + public boolean isEmpty() { + return false; + } + }, rect.top, rect.left, rect.bottom, rect.right, -1, null); + + Amenity res = null; + for (Amenity amenity : amenities) { + Long amenityId = amenity.getId() >> 1; + if (amenityId == id && !amenity.isClosed()) { + res = amenity; + break; + } + } + if (res == null && names != null && names.size() > 0) { + for (Amenity amenity : amenities) { + for (String name : names) { + if (name.equals(amenity.getName()) && !amenity.isClosed()) { + res = amenity; + break; + } + } + if (res != null) { + break; + } + } + } + + return res; + } + + public int getDefaultRadiusPoi(RotatedTileBox tb) { + int r; + final double zoom = tb.getZoom(); + if (zoom <= 15) { + r = 10; + } else if (zoom <= 16) { + r = 14; + } else if (zoom <= 17) { + r = 16; + } else { + r = 18; + } + return (int) (r * tb.getDensity()); + } + + protected int getIconSize(Context ctx) { + return ctx.getResources().getDimensionPixelSize(R.dimen.favorites_icon_outline_size); + } + + public Rect getIconDestinationRect(float x, float y, int width, int height, float scale) { + int scaledWidth = width; + int scaledHeight = height; + if (scale != 1.0f) { + scaledWidth = (int) (width * scale); + scaledHeight = (int) (height * scale); + } + Rect rect = new Rect(0, 0, scaledWidth, scaledHeight); + rect.offset((int) x - scaledWidth / 2, (int) y - scaledHeight / 2); + return rect; + } + + public int getScaledTouchRadius(OsmandApplication app, int radiusPoi) { + float textScale = app.getSettings().TEXT_SCALE.get(); + if (textScale < 1.0f) { + textScale = 1.0f; + } + return (int) textScale * radiusPoi; + } + + public void setMapButtonIcon(ImageView imageView, Drawable icon) { + int btnSizePx = imageView.getLayoutParams().height; + int iconSizePx = imageView.getContext().getResources().getDimensionPixelSize(R.dimen.map_widget_icon); + int iconPadding = (btnSizePx - iconSizePx) / 2; + imageView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding); + imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + imageView.setImageDrawable(icon); + } + + public abstract class MapLayerData { + public int ZOOM_THRESHOLD = 1; + public RotatedTileBox queriedBox; + protected T results; + protected Task currentTask; + protected Task pendingTask; + + public RotatedTileBox getQueriedBox() { + return queriedBox; + } + + public T getResults() { + return results; + } + + public boolean queriedBoxContains(final RotatedTileBox queriedData, final RotatedTileBox newBox) { + return queriedData != null && queriedData.containsTileBox(newBox) && Math.abs(queriedData.getZoom() - newBox.getZoom()) <= ZOOM_THRESHOLD; + } + + public void queryNewData(RotatedTileBox tileBox) { + if (!queriedBoxContains(queriedBox, tileBox)) { + Task ct = currentTask; + if (ct == null || !queriedBoxContains(ct.getDataBox(), tileBox)) { + RotatedTileBox original = tileBox.copy(); + RotatedTileBox extended = original.copy(); + extended.increasePixelDimensions(tileBox.getPixWidth() / 2, tileBox.getPixHeight() / 2); + Task task = new Task(original, extended); + if (currentTask == null) { + executeTaskInBackground(task); + } else { + pendingTask = task; + } + } + + } + } + + public void layerOnPreExecute() { + } + + public void layerOnPostExecute() { + } + + public boolean isInterrupted() { + return pendingTask != null; + } + + protected abstract T calculateResult(RotatedTileBox tileBox); + + public class Task extends AsyncTask { + private RotatedTileBox dataBox; + private RotatedTileBox requestedBox; + + public Task(RotatedTileBox requestedBox, RotatedTileBox dataBox) { + this.requestedBox = requestedBox; + this.dataBox = dataBox; + } + + public RotatedTileBox getOriginalBox() { + return requestedBox; + } + + public RotatedTileBox getDataBox() { + return dataBox; + } + + @Override + protected T doInBackground(Object... params) { + if (queriedBoxContains(queriedBox, dataBox)) { + return null; + } + return calculateResult(dataBox); + } + + @Override + protected void onPreExecute() { + currentTask = this; + layerOnPreExecute(); + } + + @Override + protected void onPostExecute(T result) { + if (result != null) { + queriedBox = dataBox; + results = result; + } + currentTask = null; + if (pendingTask != null) { + executeTaskInBackground(pendingTask); + pendingTask = null; + } else { + layerOnPostExecute(); + } + } + } + + public void clearCache() { + results = null; + queriedBox = null; + + } + + } + + public static class RenderingLineAttributes { + protected int cachedHash; + public Paint paint; + public Paint customColorPaint; + public int customColor = 0; + public int defaultWidth = 0; + public int defaultColor = 0; + public boolean isPaint2; + public Paint paint2; + public int defaultWidth2 = 0; + public boolean isPaint3; + public Paint paint3; + public int defaultWidth3 = 0; + public Paint shadowPaint; + public boolean isShadowPaint; + public int defaultShadowWidthExtent = 2; + public Paint paint_1; + public boolean isPaint_1; + public int defaultWidth_1 = 0; + private String renderingAttribute; + + public RenderingLineAttributes(String renderingAttribute) { + this.renderingAttribute = renderingAttribute; + paint = initPaint(); + customColorPaint = new Paint(paint); + paint2 = initPaint(); + paint3 = initPaint(); + paint_1 = initPaint(); + shadowPaint = initPaint(); + } + + + private Paint initPaint() { + Paint paint = new Paint(); + paint.setStyle(Paint.Style.STROKE); + paint.setAntiAlias(true); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + return paint; + } + + + public boolean updatePaints(OsmandApplication app, DrawSettings settings, RotatedTileBox tileBox) { + OsmandRenderer renderer = app.getResourceManager().getRenderer().getRenderer(); + RenderingRulesStorage rrs = app.getRendererRegistry().getCurrentSelectedRenderer(); + final boolean isNight = settings != null && settings.isNightMode(); + int hsh = calculateHash(rrs, isNight, tileBox.getDensity()); + if (hsh != cachedHash) { + cachedHash = hsh; + if (rrs != null) { + RenderingRuleSearchRequest req = new RenderingRuleSearchRequest(rrs); + req.setBooleanFilter(rrs.PROPS.R_NIGHT_MODE, isNight); + if (req.searchRenderingAttribute(renderingAttribute)) { + OsmandRenderer.RenderingContext rc = new OsmandRenderer.RenderingContext(app); + rc.setDensityValue((float) tileBox.getDensity()); + // cachedColor = req.getIntPropertyValue(rrs.PROPS.R_COLOR); + renderer.updatePaint(req, paint, 0, false, rc); + isPaint2 = renderer.updatePaint(req, paint2, 1, false, rc); + if (paint2.getStrokeWidth() == 0 && defaultWidth2 != 0) { + paint2.setStrokeWidth(defaultWidth2); + } + isPaint3 = renderer.updatePaint(req, paint3, 2, false, rc); + if (paint3.getStrokeWidth() == 0 && defaultWidth3 != 0) { + paint3.setStrokeWidth(defaultWidth3); + } + isPaint_1 = renderer.updatePaint(req, paint_1, -1, false, rc); + if (paint_1.getStrokeWidth() == 0 && defaultWidth_1 != 0) { + paint_1.setStrokeWidth(defaultWidth_1); + } + isShadowPaint = req.isSpecified(rrs.PROPS.R_SHADOW_RADIUS); + if (isShadowPaint) { + ColorFilter cf = new PorterDuffColorFilter( + req.getIntPropertyValue(rrs.PROPS.R_SHADOW_COLOR), PorterDuff.Mode.SRC_IN); + shadowPaint.setColorFilter(cf); + shadowPaint.setStrokeWidth(paint.getStrokeWidth() + defaultShadowWidthExtent + * rc.getComplexValue(req, rrs.PROPS.R_SHADOW_RADIUS)); + } + } else { + System.err.println("Rendering attribute route is not found !"); + } + updateDefaultColor(paint, defaultColor); + if (paint.getStrokeWidth() == 0 && defaultWidth != 0) { + paint.setStrokeWidth(defaultWidth); + } + customColorPaint = new Paint(paint); + } + return true; + } + return false; + } + + + private void updateDefaultColor(Paint paint, int defaultColor) { + if ((paint.getColor() == 0 || paint.getColor() == Color.BLACK) && defaultColor != 0) { + paint.setColor(defaultColor); + } + } + + private int calculateHash(Object... o) { + return Arrays.hashCode(o); + } + + public void drawPath(Canvas canvas, Path path) { + if (isPaint_1) { + canvas.drawPath(path, paint_1); + } + if (isShadowPaint) { + canvas.drawPath(path, shadowPaint); + } + if (customColor != 0) { + customColorPaint.setColor(customColor); + canvas.drawPath(path, customColorPaint); + } else { + canvas.drawPath(path, paint); + } + if (isPaint2) { + canvas.drawPath(path, paint2); + } + if (isPaint3) { + canvas.drawPath(path, paint3); + } + } + } +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/OsmandMapTileMiniView.java b/OsmAnd/src/net/osmand/plus/server/map/OsmandMapTileMiniView.java new file mode 100644 index 0000000000..ed51b76e78 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/OsmandMapTileMiniView.java @@ -0,0 +1,1314 @@ +package net.osmand.plus.server.map; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.*; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.view.*; +import android.widget.Toast; +import net.osmand.AndroidUtils; +import net.osmand.PlatformUtil; +import net.osmand.access.AccessibilityActionsProvider; +import net.osmand.core.android.MapRendererView; +import net.osmand.data.LatLon; +import net.osmand.data.QuadPoint; +import net.osmand.data.QuadPointDouble; +import net.osmand.data.RotatedTileBox; +import net.osmand.map.IMapLocationListener; +import net.osmand.map.MapTileDownloader; +import net.osmand.plus.OsmAndConstants; +import net.osmand.plus.OsmAndFormatter; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.helpers.TwoFingerTapDetector; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.views.*; +import net.osmand.plus.views.layers.ContextMenuLayer; +import net.osmand.render.RenderingRuleSearchRequest; +import net.osmand.render.RenderingRuleStorageProperties; +import net.osmand.render.RenderingRulesStorage; +import net.osmand.util.MapUtils; +import org.apache.commons.logging.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class OsmandMapTileMiniView implements MapTileDownloader.IMapDownloaderCallback { + private static final int MAP_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 4; + private static final int MAP_FORCE_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 5; + private static final int BASE_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 3; + protected final static int LOWEST_ZOOM_TO_ROTATE = 9; + private static final int MAP_DEFAULT_COLOR = 0xffebe7e4; + private boolean MEASURE_FPS = false; + private FPSMeasurement main = new FPSMeasurement(); + private FPSMeasurement additional = new FPSMeasurement(); + private View view; + private OsmandApplication application; + protected OsmandSettings settings = null; + private CanvasColors canvasColors = null; + private Boolean nightMode = null; + public Bitmap currentCanvas = null; + + private class CanvasColors { + int colorDay = MAP_DEFAULT_COLOR; + int colorNight = MAP_DEFAULT_COLOR; + } + + private class FPSMeasurement { + int fpsMeasureCount = 0; + int fpsMeasureMs = 0; + long fpsFirstMeasurement = 0; + float fps; + + void calculateFPS(long start, long end) { + fpsMeasureMs += end - start; + fpsMeasureCount++; + if (fpsMeasureCount > 10 || (start - fpsFirstMeasurement) > 400) { + fpsFirstMeasurement = start; + fps = (1000f * fpsMeasureCount / fpsMeasureMs); + fpsMeasureCount = 0; + fpsMeasureMs = 0; + } + } + } + + + protected static final int emptyTileDivisor = 16; + + + public interface OnTrackBallListener { + public boolean onTrackBallEvent(MotionEvent e); + } + + public interface OnLongClickListener { + public boolean onLongPressEvent(PointF point); + } + + public interface OnClickListener { + public boolean onPressEvent(PointF point); + } + + public interface OnDrawMapListener { + public void onDrawOverMap(); + } + + protected static final Log LOG = PlatformUtil.getLog(net.osmand.plus.views.OsmandMapTileView.class); + + + private RotatedTileBox currentViewport; + + private float rotate; // accumulate + + private int mapPosition; + private int mapPositionX; + + private float mapRatioX; + private float mapRatioY; + + private boolean showMapPosition = true; + + private IMapLocationListener locationListener; + + private net.osmand.plus.views.OsmandMapTileView.OnLongClickListener onLongClickListener; + + private net.osmand.plus.views.OsmandMapTileView.OnClickListener onClickListener; + + private net.osmand.plus.views.OsmandMapTileView.OnTrackBallListener trackBallDelegate; + + private AccessibilityActionsProvider accessibilityActions; + + private List layers = new ArrayList<>(); + + private BaseMapLayer mainLayer; + + private Map zOrders = new HashMap(); + + private net.osmand.plus.views.OsmandMapTileView.OnDrawMapListener onDrawMapListener; + + // UI Part + // handler to refresh map (in ui thread - ui thread is not necessary, but msg queue is required). + protected Handler handler; + private Handler baseHandler; + + private AnimateDraggingMapThread animatedDraggingThread; + + Paint paintGrayFill; + Paint paintBlackFill; + Paint paintWhiteFill; + Paint paintCenter; + + private DisplayMetrics dm; + private MapRendererView mapRenderer; + + private Bitmap bufferBitmap; + private RotatedTileBox bufferImgLoc; + private Bitmap bufferBitmapTmp; + + private Paint paintImg; + + private GestureDetector gestureDetector; + private MultiTouchSupport multiTouchSupport; + private DoubleTapScaleDetector doubleTapScaleDetector; + private TwoFingerTapDetector twoFingersTapDetector; + //private boolean afterTwoFingersTap = false; + private boolean afterDoubleTap = false; + private boolean wasMapLinkedBeforeGesture = false; + + private LatLon firstTouchPointLatLon; + private LatLon secondTouchPointLatLon; + private boolean multiTouch; + private long multiTouchStartTime; + private long multiTouchEndTime; + private boolean wasZoomInMultiTouch; + private float elevationAngle; + + public OsmandMapTileMiniView(OsmandApplication app, int w, int h) { + this.application = app; + init(w, h); + } + + // ///////////////////////////// INITIALIZING UI PART /////////////////////////////////// + public void init(int w, int h) { + settings = application.getSettings(); + + paintGrayFill = new Paint(); + paintGrayFill.setColor(Color.GRAY); + paintGrayFill.setStyle(Paint.Style.FILL); + // when map rotate + paintGrayFill.setAntiAlias(true); + + paintBlackFill = new Paint(); + paintBlackFill.setColor(Color.BLACK); + paintBlackFill.setStyle(Paint.Style.FILL); + // when map rotate + paintBlackFill.setAntiAlias(true); + + paintWhiteFill = new Paint(); + paintWhiteFill.setColor(Color.WHITE); + paintWhiteFill.setStyle(Paint.Style.FILL); + // when map rotate + paintWhiteFill.setAntiAlias(true); + + paintCenter = new Paint(); + paintCenter.setStyle(Paint.Style.STROKE); + paintCenter.setColor(Color.rgb(60, 60, 60)); + paintCenter.setStrokeWidth(2); + paintCenter.setAntiAlias(true); + + paintImg = new Paint(); + paintImg.setFilterBitmap(true); +// paintImg.setDither(true); + + //handler = new Handler(); + //baseHandler = new Handler(application.getResourceManager().getRenderingBufferImageThread().getLooper()); + + WindowManager mgr = (WindowManager) application.getSystemService(Context.WINDOW_SERVICE); + dm = new DisplayMetrics(); + mgr.getDefaultDisplay().getMetrics(dm); + LatLon ll = settings.getLastKnownMapLocation(); + currentViewport = new RotatedTileBox.RotatedTileBoxBuilder(). + setLocation(ll.getLatitude(), ll.getLongitude()).setZoom(settings.getLastKnownMapZoom()). + setPixelDimensions(w, h).build(); + currentViewport.setDensity(dm.density); + currentViewport.setMapDensity(getSettingsMapDensity()); + + elevationAngle = settings.getLastKnownMapElevation(); + } + + public void setView(View view) { + this.view = view; + view.setClickable(true); + view.setLongClickable(true); + view.setFocusable(true); + if (Build.VERSION.SDK_INT >= 26) { + view.setDefaultFocusHighlightEnabled(false); + } + refreshMap(true); + } + + public Boolean onKeyDown(int keyCode, KeyEvent event) { + return application.accessibilityEnabled() ? false : null; + } + + public boolean isLayerVisible(OsmandMapMiniLayer layer) { + return layers.contains(layer); + } + + public float getZorder(OsmandMapMiniLayer layer) { + Float z = zOrders.get(layer); + if (z == null) { + return 10; + } + return z; + } + + public synchronized void addLayer(OsmandMapMiniLayer layer, float zOrder) { + int i = 0; + for (i = 0; i < layers.size(); i++) { + if (zOrders.get(layers.get(i)) > zOrder) { + break; + } + } + layer.initLayer(this); + layers.add(i, layer); + zOrders.put(layer, zOrder); + } + + public synchronized void removeLayer(OsmandMapMiniLayer layer) { + while (layers.remove(layer)) ; + zOrders.remove(layer); + layer.destroyLayer(); + } + + public synchronized void removeAllLayers() { + while (layers.size() > 0) { + removeLayer(layers.get(0)); + } + } + + public List getLayers() { + return layers; + } + + @SuppressWarnings("unchecked") + public T getLayerByClass(Class cl) { + for (OsmandMapMiniLayer lr : layers) { + if (cl.isInstance(lr)) { + return (T) lr; + } + } + return null; + } + + public int getViewHeight() { + if (view != null) { + return view.getHeight(); + } else { + return 0; + } + } + + public OsmandApplication getApplication() { + return application; + } + + // ///////////////////////// NON UI PART (could be extracted in common) ///////////////////////////// + public LatLon getFirstTouchPointLatLon() { + return firstTouchPointLatLon; + } + + public LatLon getSecondTouchPointLatLon() { + return secondTouchPointLatLon; + } + + public boolean isMultiTouch() { + return multiTouch; + } + + public long getMultiTouchStartTime() { + return multiTouchStartTime; + } + + public long getMultiTouchEndTime() { + return multiTouchEndTime; + } + + public boolean isWasZoomInMultiTouch() { + return wasZoomInMultiTouch; + } + + public boolean mapGestureAllowed(OsmandMapMiniLayer.MapGestureType type) { +// for (OsmandMapMiniLayer layer : layers) { +// if (!layer.isMapGestureAllowed(type)) { +// return false; +// } +// } + return true; + } + + public void setIntZoom(int zoom) { + zoom = zoom < getMinZoom() ? getMinZoom() : zoom; + if (mainLayer != null) { + animatedDraggingThread.stopAnimating(); + currentViewport.setZoomAndAnimation(zoom, 0, 0); + if (zoom <= LOWEST_ZOOM_TO_ROTATE) { + rotate = 0; + } + currentViewport.setRotate(rotate); + refreshMap(); + } + } + + public void setComplexZoom(int zoom, double mapDensity) { + if (mainLayer != null && zoom <= getMaxZoom() && zoom >= getMinZoom()) { + animatedDraggingThread.stopAnimating(); + currentViewport.setZoomAndAnimation(zoom, 0); + currentViewport.setMapDensity(mapDensity); + if (zoom <= LOWEST_ZOOM_TO_ROTATE) { + rotate = 0; + } + currentViewport.setRotate(rotate); + refreshMap(); + } + } + + + public boolean isMapRotateEnabled() { + return getZoom() > LOWEST_ZOOM_TO_ROTATE; + } + + public void setRotate(float rotate, boolean force) { + if (isMapRotateEnabled()) { + float diff = MapUtils.unifyRotationDiff(rotate, getRotate()); + if (Math.abs(diff) > 5 || force) { // check smallest rotation + animatedDraggingThread.startRotate(rotate); + } + } + } + + public boolean isShowMapPosition() { + return showMapPosition; + } + + public void setShowMapPosition(boolean showMapPosition) { + this.showMapPosition = showMapPosition; + } + + public float getRotate() { + return currentViewport.getRotate(); + } + + + public void setLatLon(double latitude, double longitude) { + animatedDraggingThread.stopAnimating(); + currentViewport.setLatLonCenter(latitude, longitude); + refreshMap(); + } + + public double getLatitude() { + return currentViewport.getLatitude(); + } + + public double getLongitude() { + return currentViewport.getLongitude(); + } + + public int getZoom() { + return currentViewport.getZoom(); + } + + public float getElevationAngle() { + return elevationAngle; + } + + public double getZoomFractionalPart() { + return currentViewport.getZoomFloatPart(); + } + + public double getSettingsMapDensity() { + return (getSettings().MAP_DENSITY.get()) * Math.max(1, getDensity()); + } + + + public boolean isZooming() { + return currentViewport.isZoomAnimated(); + } + + public void setMapLocationListener(IMapLocationListener l) { + locationListener = l; + } + + /** + * Adds listener to control when map is dragging + */ + public IMapLocationListener setMapLocationListener() { + return locationListener; + } + + public void setOnDrawMapListener(net.osmand.plus.views.OsmandMapTileView.OnDrawMapListener onDrawMapListener) { + this.onDrawMapListener = onDrawMapListener; + } + + // ////////////////////////////// DRAWING MAP PART ///////////////////////////////////////////// + public BaseMapLayer getMainLayer() { + return mainLayer; + } + + public void setMainLayer(BaseMapLayer mainLayer) { + this.mainLayer = mainLayer; + int zoom = currentViewport.getZoom(); + + if (getMaxZoom() < zoom) { + zoom = getMaxZoom(); + } + if (getMinZoom() > zoom) { + zoom = getMinZoom(); + } + currentViewport.setZoomAndAnimation(zoom, 0, 0); + refreshMap(); + } + + public int getMapPosition() { + return mapPosition; + } + + public void setMapPosition(int type) { + this.mapPosition = type; + } + + public void setMapPositionX(int type) { + this.mapPositionX = type; + } + + public void setCustomMapRatio(float ratioX, float ratioY) { + this.mapRatioX = ratioX; + this.mapRatioY = ratioY; + } + + public void restoreMapRatio() { + RotatedTileBox box = currentViewport.copy(); + float rx = (float) box.getCenterPixelX() / box.getPixWidth(); + float ry = (float) box.getCenterPixelY() / box.getPixHeight(); + if (mapPosition == OsmandSettings.BOTTOM_CONSTANT) { + ry -= 0.35; + } + box.setCenterLocation(rx, ry); + LatLon screenCenter = box.getLatLonFromPixel(box.getPixWidth() / 2f, box.getPixHeight() / 2f); + mapRatioX = 0; + mapRatioY = 0; + setLatLon(screenCenter.getLatitude(), screenCenter.getLongitude()); + } + + public boolean hasCustomMapRatio() { + return mapRatioX != 0 && mapRatioY != 0; + } + + public OsmandSettings getSettings() { + return settings; + } + + public int getMaxZoom() { + if (mainLayer != null) { + return mainLayer.getMaximumShownMapZoom(); + } + return BaseMapLayer.DEFAULT_MAX_ZOOM; + } + + public int getMinZoom() { + if (mainLayer != null) { + return mainLayer.getMinimumShownMapZoom() + 1; + } + return BaseMapLayer.DEFAULT_MIN_ZOOM; + } + + private void drawBasemap(Canvas canvas) { + if (bufferImgLoc != null) { + float rot = -bufferImgLoc.getRotate(); + canvas.rotate(rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); + final RotatedTileBox calc = currentViewport.copy(); + calc.setRotate(bufferImgLoc.getRotate()); + + int cz = getZoom(); + QuadPointDouble lt = bufferImgLoc.getLeftTopTile(cz); + QuadPointDouble rb = bufferImgLoc.getRightBottomTile(cz); + final float x1 = calc.getPixXFromTile(lt.x, lt.y, cz); + final float x2 = calc.getPixXFromTile(rb.x, rb.y, cz); + final float y1 = calc.getPixYFromTile(lt.x, lt.y, cz); + final float y2 = calc.getPixYFromTile(rb.x, rb.y, cz); +// LatLon lt = bufferImgLoc.getLeftTopLatLon(); +// LatLon rb = bufferImgLoc.getRightBottomLatLon(); +// final float x1 = calc.getPixXFromLatLon(lt.getLatitude(), lt.getLongitude()); +// final float x2 = calc.getPixXFromLatLon(rb.getLatitude(), rb.getLongitude()); +// final float y1 = calc.getPixYFromLatLon(lt.getLatitude(), lt.getLongitude()); +// final float y2 = calc.getPixYFromLatLon(rb.getLatitude(), rb.getLongitude()); + if (!bufferBitmap.isRecycled()) { + RectF rct = new RectF(x1, y1, x2, y2); + canvas.drawBitmap(bufferBitmap, null, rct, paintImg); + currentCanvas = bufferBitmap; + } + canvas.rotate(-rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); + } + } + + private void refreshBaseMapInternal(RotatedTileBox tileBox, OsmandMapMiniLayer.DrawSettings drawSettings) { + if (tileBox.getPixHeight() == 0 || tileBox.getPixWidth() == 0) { + return; + } + if (bufferBitmapTmp == null || tileBox.getPixHeight() != bufferBitmapTmp.getHeight() + || tileBox.getPixWidth() != bufferBitmapTmp.getWidth()) { + bufferBitmapTmp = Bitmap.createBitmap(tileBox.getPixWidth(), tileBox.getPixHeight(), Bitmap.Config.ARGB_8888); + } + long start = SystemClock.elapsedRealtime(); + final QuadPoint c = tileBox.getCenterPixelPoint(); + Canvas canvas = new Canvas(bufferBitmapTmp); + fillCanvas(canvas, drawSettings); + for (int i = 0; i < layers.size(); i++) { + try { + OsmandMapMiniLayer layer = layers.get(i); + canvas.save(); + // rotate if needed + if (!layer.drawInScreenPixels()) { + canvas.rotate(tileBox.getRotate(), c.x, c.y); + } + layer.onPrepareBufferImage(canvas, tileBox, drawSettings); + canvas.restore(); + } catch (IndexOutOfBoundsException e) { + // skip it + canvas.restore(); + } + } + Bitmap t = bufferBitmap; + synchronized (this) { + bufferImgLoc = tileBox; + bufferBitmap = bufferBitmapTmp; + bufferBitmapTmp = t; + currentCanvas = bufferBitmap; + } + long end = SystemClock.elapsedRealtime(); + additional.calculateFPS(start, end); + } + + public void refreshMapInternal(OsmandMapMiniLayer.DrawSettings drawSettings) { + if (view == null) { + return; + } + final float ratioy; + if (mapRatioY != 0) { + ratioy = mapRatioY; + } else if (mapPosition == OsmandSettings.BOTTOM_CONSTANT) { + ratioy = 0.85f; + } else if (mapPosition == OsmandSettings.MIDDLE_BOTTOM_CONSTANT) { + ratioy = 0.70f; + } else if (mapPosition == OsmandSettings.MIDDLE_TOP_CONSTANT) { + ratioy = 0.25f; + } else { + ratioy = 0.5f; + } + final float ratiox; + if (mapRatioX != 0) { + ratiox = mapRatioX; + } else if (mapPosition == OsmandSettings.LANDSCAPE_MIDDLE_RIGHT_CONSTANT) { + ratiox = 0.7f; + } else { + boolean isLayoutRtl = AndroidUtils.isLayoutRtl(application); + ratiox = mapPositionX == 0 ? 0.5f : (isLayoutRtl ? 0.25f : 0.75f); + } + final int cy = (int) (ratioy * view.getHeight()); + final int cx = (int) (ratiox * view.getWidth()); + if (currentViewport.getPixWidth() != view.getWidth() || currentViewport.getPixHeight() != view.getHeight() || + currentViewport.getCenterPixelY() != cy || + currentViewport.getCenterPixelX() != cx) { + currentViewport.setPixelDimensions(view.getWidth(), view.getHeight(), ratiox, ratioy); + refreshBufferImage(drawSettings); + } + if (view instanceof SurfaceView) { + SurfaceHolder holder = ((SurfaceView) view).getHolder(); + long ms = SystemClock.elapsedRealtime(); + synchronized (holder) { + Canvas canvas = holder.lockCanvas(); + if (canvas != null) { + try { + // make copy to avoid concurrency + RotatedTileBox viewportToDraw = currentViewport.copy(); + drawOverMap(canvas, viewportToDraw, drawSettings); + } finally { + holder.unlockCanvasAndPost(canvas); + } + } + if (MEASURE_FPS) { + main.calculateFPS(ms, SystemClock.elapsedRealtime()); + } + } + } else { + view.invalidate(); + } + } + + private void fillCanvas(Canvas canvas, OsmandMapMiniLayer.DrawSettings drawSettings) { + int color = MAP_DEFAULT_COLOR; + CanvasColors canvasColors = this.canvasColors; + if (canvasColors == null) { + canvasColors = updateCanvasColors(); + this.canvasColors = canvasColors; + } + if (canvasColors != null) { + color = drawSettings.isNightMode() ? canvasColors.colorNight : canvasColors.colorDay; + } + canvas.drawColor(color); + } + + public void resetDefaultColor() { + canvasColors = null; + } + + private CanvasColors updateCanvasColors() { + CanvasColors canvasColors = null; + RenderingRulesStorage rrs = application.getRendererRegistry().getCurrentSelectedRenderer(); + if (rrs != null) { + canvasColors = new CanvasColors(); + RenderingRuleSearchRequest req = new RenderingRuleSearchRequest(rrs); + req.setBooleanFilter(rrs.PROPS.R_NIGHT_MODE, false); + if (req.searchRenderingAttribute(RenderingRuleStorageProperties.A_DEFAULT_COLOR)) { + canvasColors.colorDay = req.getIntPropertyValue(req.ALL.R_ATTR_COLOR_VALUE); + } + req = new RenderingRuleSearchRequest(rrs); + req.setBooleanFilter(rrs.PROPS.R_NIGHT_MODE, true); + if (req.searchRenderingAttribute(RenderingRuleStorageProperties.A_DEFAULT_COLOR)) { + canvasColors.colorNight = req.getIntPropertyValue(req.ALL.R_ATTR_COLOR_VALUE); + } + } + return canvasColors; + } + + public boolean isMeasureFPS() { + return MEASURE_FPS; + } + + public void setMeasureFPS(boolean measureFPS) { + MEASURE_FPS = measureFPS; + } + + public float getFPS() { + return main.fps; + } + + public float getSecondaryFPS() { + return additional.fps; + } + + public boolean isAnimatingZoom() { + return animatedDraggingThread.isAnimatingZoom(); + } + + @SuppressLint("WrongCall") + public void drawOverMap(Canvas canvas, RotatedTileBox tileBox, OsmandMapMiniLayer.DrawSettings drawSettings) { + if (mapRenderer == null) { + fillCanvas(canvas, drawSettings); + } + final QuadPoint c = tileBox.getCenterPixelPoint(); + synchronized (this) { + //if (bufferBitmap != null && !bufferBitmap.isRecycled() && mapRenderer == null) { + //canvas.save(); + //canvas.rotate(tileBox.getRotate(), c.x, c.y); + drawBasemap(canvas); + //canvas.restore(); + //} + } + + if (onDrawMapListener != null) { + onDrawMapListener.onDrawOverMap(); + } + + for (int i = 0; i < layers.size(); i++) { + try { + OsmandMapMiniLayer layer = layers.get(i); + canvas.save(); + // rotate if needed + if (!layer.drawInScreenPixels()) { + canvas.rotate(tileBox.getRotate(), c.x, c.y); + } + //if (mapRenderer != null) { + layer.onPrepareBufferImage(canvas, tileBox, drawSettings); + //} + try{ + layer.onDraw(canvas, tileBox, drawSettings); + } + catch (Exception e){ + e.printStackTrace(); + } + canvas.restore(); + } catch (IndexOutOfBoundsException e) { + // skip it + } + } + if (showMapPosition || animatedDraggingThread.isAnimatingZoom()) { + drawMapPosition(canvas, c.x, c.y); + } else if (multiTouchSupport.isInZoomMode()) { + drawMapPosition(canvas, multiTouchSupport.getCenterPoint().x, multiTouchSupport.getCenterPoint().y); + } else if (doubleTapScaleDetector.isInZoomMode()) { + drawMapPosition(canvas, doubleTapScaleDetector.getCenterX(), doubleTapScaleDetector.getCenterY()); + } + } + + + protected void drawMapPosition(Canvas canvas, float x, float y) { + canvas.drawCircle(x, y, 3 * dm.density, paintCenter); + canvas.drawCircle(x, y, 7 * dm.density, paintCenter); + } + + private void refreshBufferImage(final OsmandMapMiniLayer.DrawSettings drawSettings) { + if (mapRenderer != null) { + return; + } + + OsmandMapMiniLayer.DrawSettings param = drawSettings; + Boolean currentNightMode = nightMode; + param = new OsmandMapMiniLayer.DrawSettings(currentNightMode, true); + refreshBaseMapInternal(currentViewport.copy(), param); + return; + +// if (!baseHandler.hasMessages(BASE_REFRESH_MESSAGE) || drawSettings.isUpdateVectorRendering()) { +// Message msg = Message.obtain(baseHandler, new Runnable() { +// @Override +// public void run() { +// baseHandler.removeMessages(BASE_REFRESH_MESSAGE); +// try { +// OsmandMapMiniLayer.DrawSettings param = drawSettings; +// Boolean currentNightMode = nightMode; +// if (currentNightMode != null && currentNightMode != param.isNightMode()) { +// param = new OsmandMapMiniLayer.DrawSettings(currentNightMode, true); +// resetDefaultColor(); +// } +// if (handler.hasMessages(MAP_FORCE_REFRESH_MESSAGE)) { +// if (!param.isUpdateVectorRendering()) { +// param = new OsmandMapMiniLayer.DrawSettings(drawSettings.isNightMode(), true); +// } +// handler.removeMessages(MAP_FORCE_REFRESH_MESSAGE); +// } +// refreshBaseMapInternal(currentViewport.copy(), param); +// //sendRefreshMapMsg(param, 0); +// } catch (Exception e) { +// LOG.error(e.getMessage(), e); +// } +// } +// }); +// msg.what = drawSettings.isUpdateVectorRendering() ? MAP_FORCE_REFRESH_MESSAGE : BASE_REFRESH_MESSAGE; +// // baseHandler.sendMessageDelayed(msg, 0); +// baseHandler.sendMessage(msg); +// } + } + + // this method could be called in non UI thread + public void refreshMap() { + refreshMap(false); + } + + // this method could be called in non UI thread + public void refreshMap(final boolean updateVectorRendering) { + + boolean nightMode = application.getDaynightHelper().isNightMode(); + Boolean currentNightMode = this.nightMode; + boolean forceUpdateVectorDrawing = currentNightMode != null && currentNightMode != nightMode; + if (forceUpdateVectorDrawing) { + resetDefaultColor(); + } + this.nightMode = nightMode; + OsmandMapMiniLayer.DrawSettings drawSettings = new OsmandMapMiniLayer.DrawSettings(nightMode, updateVectorRendering || forceUpdateVectorDrawing); + //sendRefreshMapMsg(drawSettings, 20); + refreshBufferImage(drawSettings); + } + + private void sendRefreshMapMsg(final OsmandMapMiniLayer.DrawSettings drawSettings, int delay) { + if (!handler.hasMessages(MAP_REFRESH_MESSAGE) || drawSettings.isUpdateVectorRendering()) { + Message msg = Message.obtain(handler, new Runnable() { + @Override + public void run() { + OsmandMapMiniLayer.DrawSettings param = drawSettings; + handler.removeMessages(MAP_REFRESH_MESSAGE); + + refreshMapInternal(param); + } + }); + msg.what = MAP_REFRESH_MESSAGE; + if (delay > 0) { + handler.sendMessageDelayed(msg, delay); + } else { + handler.sendMessage(msg); + } + } + } + + @Override + public void tileDownloaded(MapTileDownloader.DownloadRequest request) { + // force to refresh map because image can be loaded from different threads + // and threads can block each other especially for sqlite images when they + // are inserting into db they block main thread + refreshMap(); + } + + // ///////////////////////////////// DRAGGING PART /////////////////////////////////////// + + public net.osmand.data.RotatedTileBox getCurrentRotatedTileBox() { + return currentViewport; + } + + public float getDensity() { + return currentViewport.getDensity(); + } + + public float getScaleCoefficient() { + float scaleCoefficient = getDensity(); + if (Math.min(dm.widthPixels / (dm.density * 160), dm.heightPixels / (dm.density * 160)) > 2.5f) { + // large screen + scaleCoefficient *= 1.5f; + } + return scaleCoefficient; + } + + /** + * These methods do not consider rotating + */ + protected void dragToAnimate(float fromX, float fromY, float toX, float toY, boolean notify) { + float dx = (fromX - toX); + float dy = (fromY - toY); + moveTo(dx, dy); + if (locationListener != null && notify) { + locationListener.locationChanged(getLatitude(), getLongitude(), this); + } + } + + protected void rotateToAnimate(float rotate) { + if (isMapRotateEnabled()) { + this.rotate = MapUtils.unifyRotationTo360(rotate); + currentViewport.setRotate(this.rotate); + refreshMap(); + } + } + + protected void setLatLonAnimate(double latitude, double longitude, boolean notify) { + currentViewport.setLatLonCenter(latitude, longitude); + refreshMap(); + if (locationListener != null && notify) { + locationListener.locationChanged(latitude, longitude, this); + } + } + + protected void setFractionalZoom(int zoom, double zoomPart, boolean notify) { + currentViewport.setZoomAndAnimation(zoom, 0, zoomPart); + refreshMap(); + if (locationListener != null && notify) { + locationListener.locationChanged(getLatitude(), getLongitude(), this); + } + } + + // for internal usage + protected void zoomToAnimate(int zoom, double zoomToAnimate, boolean notify) { + if (mainLayer != null && getMaxZoom() >= zoom && getMinZoom() <= zoom) { + currentViewport.setZoomAndAnimation(zoom, zoomToAnimate); + if (zoom <= LOWEST_ZOOM_TO_ROTATE) { + rotate = 0; + } + currentViewport.setRotate(rotate); + refreshMap(); + if (notify && locationListener != null) { + locationListener.locationChanged(getLatitude(), getLongitude(), this); + } + } + } + + public void moveTo(float dx, float dy) { + final QuadPoint cp = currentViewport.getCenterPixelPoint(); + final LatLon latlon = currentViewport.getLatLonFromPixel(cp.x + dx, cp.y + dy); + currentViewport.setLatLonCenter(latlon.getLatitude(), latlon.getLongitude()); + refreshMap(); + // do not notify here listener + + } + + public void fitRectToMap(double left, double right, double top, double bottom, + int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx) { + fitRectToMap(left, right, top, bottom, tileBoxWidthPx, tileBoxHeightPx, marginTopPx, 0); + } + + public void fitRectToMap(double left, double right, double top, double bottom, + int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx, int marginLeftPx) { + RotatedTileBox tb = currentViewport.copy(); + double border = 0.8; + int dx = 0; + int dy = 0; + + int tbw = (int) (tb.getPixWidth() * border); + int tbh = (int) (tb.getPixHeight() * border); + if (tileBoxWidthPx > 0) { + tbw = (int) (tileBoxWidthPx * border); + if (marginLeftPx > 0) { + dx = (tb.getPixWidth() - tileBoxWidthPx) / 2 - marginLeftPx; + } + } else if (tileBoxHeightPx > 0) { + tbh = (int) (tileBoxHeightPx * border); + dy = (tb.getPixHeight() - tileBoxHeightPx) / 2 - marginTopPx; + } + dy += tb.getCenterPixelY() - tb.getPixHeight() / 2; + tb.setPixelDimensions(tbw, tbh); + + double clat = bottom / 2 + top / 2; + double clon = left / 2 + right / 2; + tb.setLatLonCenter(clat, clon); + while (tb.getZoom() < 17 && tb.containsLatLon(top, left) && tb.containsLatLon(bottom, right)) { + tb.setZoom(tb.getZoom() + 1); + } + while (tb.getZoom() >= 7 && (!tb.containsLatLon(top, left) || !tb.containsLatLon(bottom, right))) { + tb.setZoom(tb.getZoom() - 1); + } + if (dy != 0 || dx != 0) { + clat = tb.getLatFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2 + dy); + clon = tb.getLonFromPixel(tb.getPixWidth() / 2 + dx, tb.getPixHeight() / 2); + } + animatedDraggingThread.startMoving(clat, clon, tb.getZoom(), true); + } + + public RotatedTileBox getTileBox(int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx) { + RotatedTileBox tb = currentViewport.copy(); + double border = 0.8; + int dy = 0; + + int tbw = (int) (tb.getPixWidth() * border); + int tbh = (int) (tb.getPixHeight() * border); + if (tileBoxWidthPx > 0) { + tbw = (int) (tileBoxWidthPx * border); + } else if (tileBoxHeightPx > 0) { + tbh = (int) (tileBoxHeightPx * border); + dy = (tb.getPixHeight() - tileBoxHeightPx) / 2 - marginTopPx; + } + dy += tb.getCenterPixelY() - tb.getPixHeight() / 2; + tb.setPixelDimensions(tbw, tbh); + + if (dy != 0) { + double clat = tb.getLatFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2 - dy); + double clon = tb.getLonFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2); + tb.setLatLonCenter(clat, clon); + } + return tb; + } + + public void fitLocationToMap(double clat, double clon, int zoom, + int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx, boolean animated) { + RotatedTileBox tb = currentViewport.copy(); + int dy = 0; + + int tbw = tb.getPixWidth(); + int tbh = tb.getPixHeight(); + if (tileBoxWidthPx > 0) { + tbw = tileBoxWidthPx; + } else if (tileBoxHeightPx > 0) { + tbh = tileBoxHeightPx; + dy = (tb.getPixHeight() - tileBoxHeightPx) / 2 - marginTopPx; + } + dy += tb.getCenterPixelY() - tb.getPixHeight() / 2; + tb.setPixelDimensions(tbw, tbh); + tb.setLatLonCenter(clat, clon); + tb.setZoom(zoom); + if (dy != 0) { + clat = tb.getLatFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2 + dy); + clon = tb.getLonFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2); + } + if (animated) { + animatedDraggingThread.startMoving(clat, clon, tb.getZoom(), true); + } else { + setLatLon(clat, clon); + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 && + event.getAction() == MotionEvent.ACTION_SCROLL && + event.getAxisValue(MotionEvent.AXIS_VSCROLL) != 0) { + final RotatedTileBox tb = getCurrentRotatedTileBox(); + final double lat = tb.getLatFromPixel(event.getX(), event.getY()); + final double lon = tb.getLonFromPixel(event.getX(), event.getY()); + int zoomDir = event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0 ? -1 : 1; + getAnimatedDraggingThread().startMoving(lat, lon, getZoom() + zoomDir, true); + return true; + } + return false; + } + + public boolean onTouchEvent(MotionEvent event) { + if (mapRenderer != null) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mapRenderer.suspendSymbolsUpdate(); + } else if (event.getAction() == MotionEvent.ACTION_UP + || event.getAction() == MotionEvent.ACTION_CANCEL) { + mapRenderer.resumeSymbolsUpdate(); + } + } +// if (twoFingersTapDetector.onTouchEvent(event)) { +// ContextMenuLayer contextMenuLayer = getLayerByClass(ContextMenuLayer.class); +// if (contextMenuLayer != null) { +// contextMenuLayer.onTouchEvent(event, getCurrentRotatedTileBox()); +// } +// return true; +// } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + animatedDraggingThread.stopAnimating(); + } + final boolean isMultiTouch = multiTouchSupport.onTouchEvent(event); + doubleTapScaleDetector.onTouchEvent(event); + if (!isMultiTouch && !doubleTapScaleDetector.isInZoomMode()) { + for (int i = layers.size() - 1; i >= 0; i--) { + layers.get(i).onTouchEvent(event, getCurrentRotatedTileBox()); + } + } + if (!doubleTapScaleDetector.isInZoomMode() && !doubleTapScaleDetector.isDoubleTapping()) { + gestureDetector.onTouchEvent(event); + } + return true; + } + + public void setMapRender(MapRendererView mapRenderer) { + this.mapRenderer = mapRenderer; + } + + public MapRendererView getMapRenderer() { + return mapRenderer; + } + + public Boolean onTrackballEvent(MotionEvent event) { + if (trackBallDelegate != null) { + return trackBallDelegate.onTrackBallEvent(event); + } + return null; + } + + public void setTrackBallDelegate(net.osmand.plus.views.OsmandMapTileView.OnTrackBallListener trackBallDelegate) { + this.trackBallDelegate = trackBallDelegate; + } + + public void setOnLongClickListener(net.osmand.plus.views.OsmandMapTileView.OnLongClickListener l) { + this.onLongClickListener = l; + } + + public void setOnClickListener(net.osmand.plus.views.OsmandMapTileView.OnClickListener l) { + this.onClickListener = l; + } + + public void setAccessibilityActions(AccessibilityActionsProvider actions) { + accessibilityActions = actions; + } + + + public AnimateDraggingMapThread getAnimatedDraggingThread() { + return animatedDraggingThread; + } + + public void showMessage(final String msg) { + handler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(application, msg, Toast.LENGTH_SHORT).show(); //$NON-NLS-1$ + } + }); + } + + + private class MapTileViewMultiTouchZoomListener implements MultiTouchSupport.MultiTouchZoomListener, + DoubleTapScaleDetector.DoubleTapZoomListener { + private PointF initialMultiTouchCenterPoint; + private RotatedTileBox initialViewport; + private float x1; + private float y1; + private float x2; + private float y2; + private LatLon initialCenterLatLon; + private boolean startRotating = false; + private static final float ANGLE_THRESHOLD = 30; + private float initialElevation; + + @Override + public void onZoomOrRotationEnded(double relativeToStart, float angleRelative) { + // 1.5 works better even on dm.density=1 devices + float dz = (float) (Math.log(relativeToStart) / Math.log(2)) * 1.5f; + setIntZoom(Math.round(dz) + initialViewport.getZoom()); + if (Math.abs(angleRelative) < ANGLE_THRESHOLD * relativeToStart || + Math.abs(angleRelative) < ANGLE_THRESHOLD / relativeToStart) { + angleRelative = 0; + } + rotateToAnimate(initialViewport.getRotate() + angleRelative); + final int newZoom = getZoom(); + if (application.accessibilityEnabled()) { + if (newZoom != initialViewport.getZoom()) { + showMessage(application.getString(R.string.zoomIs) + " " + newZoom); //$NON-NLS-1$ + } else { + final LatLon p1 = initialViewport.getLatLonFromPixel(x1, y1); + final LatLon p2 = initialViewport.getLatLonFromPixel(x2, y2); + showMessage(OsmAndFormatter.getFormattedDistance((float) MapUtils.getDistance(p1.getLatitude(), p1.getLongitude(), p2.getLatitude(), p2.getLongitude()), application)); + } + } + } + + @Override + public void onZoomEnded(double relativeToStart) { + // 1.5 works better even on dm.density=1 devices + float dz = (float) ((relativeToStart - 1) * DoubleTapScaleDetector.SCALE_PER_SCREEN); + setIntZoom(Math.round(dz) + initialViewport.getZoom()); + final int newZoom = getZoom(); + if (application.accessibilityEnabled()) { + if (newZoom != initialViewport.getZoom()) { + showMessage(application.getString(R.string.zoomIs) + " " + newZoom); //$NON-NLS-1$ + } else { + final LatLon p1 = initialViewport.getLatLonFromPixel(x1, y1); + final LatLon p2 = initialViewport.getLatLonFromPixel(x2, y2); + showMessage(OsmAndFormatter.getFormattedDistance((float) MapUtils.getDistance(p1.getLatitude(), p1.getLongitude(), p2.getLatitude(), p2.getLongitude()), application)); + } + } + } + + + @Override + public void onGestureInit(float x1, float y1, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + if (x1 != x2 || y1 != y2) { + firstTouchPointLatLon = currentViewport.getLatLonFromPixel(x1, y1); + secondTouchPointLatLon = currentViewport.getLatLonFromPixel(x2, y2); + multiTouch = true; + wasZoomInMultiTouch = false; + multiTouchStartTime = System.currentTimeMillis(); + } + } + + @Override + public void onActionPointerUp() { + multiTouch = false; + if (isZooming()) { + wasZoomInMultiTouch = true; + } else { + multiTouchEndTime = System.currentTimeMillis(); + wasZoomInMultiTouch = false; + } + } + + @Override + public void onActionCancel() { + multiTouch = false; + } + + @Override + public void onChangingViewAngle(float angle) { + setElevationAngle(initialElevation - angle); + } + + @Override + public void onChangeViewAngleStarted() { + initialElevation = elevationAngle; + } + + @Override + public void onZoomStarted(PointF centerPoint) { + initialMultiTouchCenterPoint = centerPoint; + initialViewport = getCurrentRotatedTileBox().copy(); + initialCenterLatLon = initialViewport.getLatLonFromPixel(initialMultiTouchCenterPoint.x, + initialMultiTouchCenterPoint.y); + startRotating = false; + } + + @Override + public void onZoomingOrRotating(double relativeToStart, float relAngle) { + double dz = (Math.log(relativeToStart) / Math.log(2)) * 1.5; + if (Math.abs(dz) <= 0.1) { + // keep only rotating + dz = 0; + } + if (Math.abs(relAngle) < ANGLE_THRESHOLD && !startRotating) { + relAngle = 0; + } else { + startRotating = true; + } + if (dz != 0 || relAngle != 0) { + changeZoomPosition((float) dz, relAngle); + } + } + + @Override + public void onZooming(double relativeToStart) { + double dz = (relativeToStart - 1) * DoubleTapScaleDetector.SCALE_PER_SCREEN; + changeZoomPosition((float) dz, 0); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + LOG.debug("onDoubleTap getZoom()"); + if (!doubleTapScaleDetector.isInZoomMode()) { + if (isZoomingAllowed(getZoom(), 1.1f)) { + final RotatedTileBox tb = getCurrentRotatedTileBox(); + final double lat = tb.getLatFromPixel(e.getX(), e.getY()); + final double lon = tb.getLonFromPixel(e.getX(), e.getY()); + getAnimatedDraggingThread().startMoving(lat, lon, getZoom() + 1, true); + } + afterDoubleTap = true; + return true; + } else { + return false; + } + } + + private void changeZoomPosition(float dz, float angle) { + final RotatedTileBox calc = initialViewport.copy(); + calc.setLatLonCenter(initialCenterLatLon.getLatitude(), initialCenterLatLon.getLongitude()); + float calcRotate = calc.getRotate() + angle; + calc.setRotate(calcRotate); + calc.setZoomAndAnimation(initialViewport.getZoom(), dz, initialViewport.getZoomFloatPart()); + if (multiTouch) { + wasZoomInMultiTouch = true; + } + + final QuadPoint cp = initialViewport.getCenterPixelPoint(); + // Keep zoom center fixed or flexible + LatLon r; + if (multiTouchSupport.isInZoomMode()) { + r = calc.getLatLonFromPixel(cp.x + cp.x - multiTouchSupport.getCenterPoint().x, cp.y + cp.y - multiTouchSupport.getCenterPoint().y); + } else { + r = calc.getLatLonFromPixel(cp.x + cp.x - initialMultiTouchCenterPoint.x, cp.y + cp.y - initialMultiTouchCenterPoint.y); + } + setLatLon(r.getLatitude(), r.getLongitude()); + + int baseZoom = initialViewport.getZoom(); + while (initialViewport.getZoomFloatPart() + dz > 1 && isZoomingAllowed(baseZoom, dz)) { + dz--; + baseZoom++; + } + while (initialViewport.getZoomFloatPart() + dz < 0 && isZoomingAllowed(baseZoom, dz)) { + dz++; + baseZoom--; + } + if (!isZoomingAllowed(baseZoom, dz)) { + dz = Math.signum(dz); + } + + zoomToAnimate(baseZoom, dz, !(doubleTapScaleDetector.isInZoomMode())); + rotateToAnimate(calcRotate); + } + + } + + private void setElevationAngle(float angle) { + if (angle < 35f) { + angle = 35f; + } else if (angle > 90f) { + angle = 90f; + } + this.elevationAngle = angle; + } + + private boolean isZoomingAllowed(int baseZoom, float dz) { + if (baseZoom > getMaxZoom()) { + return false; + } + if (baseZoom > getMaxZoom() - 1 && dz > 1) { + return false; + } + if (baseZoom < getMinZoom()) { + return false; + } + if (baseZoom < getMinZoom() + 1 && dz < -1) { + return false; + } + return true; + } + + public Resources getResources() { + return application.getResources(); + } +} + diff --git a/OsmAnd/src/net/osmand/plus/server/map/POIMapLayerMini.java b/OsmAnd/src/net/osmand/plus/server/map/POIMapLayerMini.java new file mode 100644 index 0000000000..d7b52fde0c --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/server/map/POIMapLayerMini.java @@ -0,0 +1,471 @@ +package net.osmand.plus.server.map; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PointF; +import android.graphics.drawable.Drawable; +import android.text.util.Linkify; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import net.osmand.AndroidUtils; +import net.osmand.PlatformUtil; +import net.osmand.ResultMatcher; +import net.osmand.ValueHolder; +import net.osmand.data.*; +import net.osmand.osm.PoiType; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.OsmandPlugin; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.base.PointImageDrawable; +import net.osmand.plus.helpers.WaypointHelper; +import net.osmand.plus.poi.PoiUIFilter; +import net.osmand.plus.render.RenderingIcons; +import net.osmand.plus.routing.IRouteInformationListener; +import net.osmand.plus.routing.RoutingHelper; +import net.osmand.plus.views.OsmandMapLayer; +import net.osmand.plus.views.OsmandMapTileView; +import net.osmand.plus.views.layers.ContextMenuLayer; +import net.osmand.plus.views.layers.MapTextLayer; +import net.osmand.util.Algorithms; + +import java.util.*; + +import static net.osmand.AndroidUtils.dpToPx; + + + import android.app.Dialog; + import android.content.Context; + import android.graphics.Canvas; + import android.graphics.PointF; + import android.graphics.drawable.Drawable; + import android.text.util.Linkify; + import android.util.TypedValue; + import android.view.View; + import android.view.ViewGroup; + import android.widget.LinearLayout; + import android.widget.LinearLayout.LayoutParams; + import android.widget.ScrollView; + import android.widget.TextView; + + import androidx.appcompat.widget.Toolbar; + import androidx.core.content.ContextCompat; + + import net.osmand.AndroidUtils; + import net.osmand.PlatformUtil; + import net.osmand.ResultMatcher; + import net.osmand.ValueHolder; + import net.osmand.data.Amenity; + import net.osmand.data.LatLon; + import net.osmand.data.PointDescription; + import net.osmand.data.QuadRect; + import net.osmand.data.QuadTree; + import net.osmand.data.RotatedTileBox; + import net.osmand.osm.PoiType; + import net.osmand.plus.OsmandApplication; + import net.osmand.plus.OsmandPlugin; + import net.osmand.plus.R; + import net.osmand.plus.activities.MapActivity; + import net.osmand.plus.base.PointImageDrawable; + import net.osmand.plus.helpers.WaypointHelper; + import net.osmand.plus.poi.PoiUIFilter; + import net.osmand.plus.render.RenderingIcons; + import net.osmand.plus.routing.IRouteInformationListener; + import net.osmand.plus.routing.RoutingHelper; + import net.osmand.plus.views.OsmandMapLayer; + import net.osmand.plus.views.OsmandMapTileView; + import net.osmand.plus.views.layers.MapTextLayer.MapTextProvider; + import net.osmand.util.Algorithms; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.Comparator; + import java.util.List; + import java.util.Set; + import java.util.TreeSet; + + import static net.osmand.AndroidUtils.dpToPx; + +public class POIMapLayerMini extends OsmandMapMiniLayer implements ContextMenuLayer.IContextMenuProvider, + MapTextLayer.MapTextProvider, IRouteInformationListener { + private static final int startZoom = 9; + + public static final org.apache.commons.logging.Log log = PlatformUtil.getLog(net.osmand.plus.views.layers.POIMapLayer.class); + + private OsmandMapTileMiniView view; + + private RoutingHelper routingHelper; + private Set filters = new TreeSet<>(); + private MapTextLayer mapTextLayer; + + /// cache for displayed POI + // Work with cache (for map copied from AmenityIndexRepositoryOdb) + private MapLayerData> data; + + private OsmandApplication app; + + public POIMapLayerMini(final OsmandApplication app) { + routingHelper = app.getRoutingHelper(); + routingHelper.addListener(this); + this.app = app; + data = new OsmandMapMiniLayer.MapLayerData>() { + + Set calculatedFilters; + + { + ZOOM_THRESHOLD = 0; + } + + @Override + public boolean isInterrupted() { + return super.isInterrupted(); + } + + @Override + public void layerOnPreExecute() { + calculatedFilters = new TreeSet<>(filters); + } + + @Override + public void layerOnPostExecute() { + + } + + @Override + protected List calculateResult(RotatedTileBox tileBox) { + QuadRect latLonBounds = tileBox.getLatLonBounds(); + if (calculatedFilters.isEmpty() || latLonBounds == null) { + return new ArrayList<>(); + } + int z = (int) Math.floor(tileBox.getZoom() + Math.log(view.getSettings().MAP_DENSITY.get()) / Math.log(2)); + + List res = new ArrayList<>(); + PoiUIFilter.combineStandardPoiFilters(calculatedFilters, app); + for (PoiUIFilter filter : calculatedFilters) { + res.addAll(filter.searchAmenities(latLonBounds.top, latLonBounds.left, + latLonBounds.bottom, latLonBounds.right, z, new ResultMatcher() { + + @Override + public boolean publish(Amenity object) { + return true; + } + + @Override + public boolean isCancelled() { + return isInterrupted(); + } + })); + } + + Collections.sort(res, new Comparator() { + @Override + public int compare(Amenity lhs, Amenity rhs) { + return lhs.getId() < rhs.getId() ? -1 : (lhs.getId().longValue() == rhs.getId().longValue() ? 0 : 1); + } + }); + + return res; + } + }; + } + + public void getAmenityFromPoint(RotatedTileBox tb, PointF point, List am) { + List objects = data.getResults(); + if (objects != null) { + int ex = (int) point.x; + int ey = (int) point.y; + int compare = getScaledTouchRadius(view.getApplication(), getRadiusPoi(tb)); + int radius = compare * 3 / 2; + try { + for (int i = 0; i < objects.size(); i++) { + Amenity n = objects.get(i); + int x = (int) tb.getPixXFromLatLon(n.getLocation().getLatitude(), n.getLocation().getLongitude()); + int y = (int) tb.getPixYFromLatLon(n.getLocation().getLatitude(), n.getLocation().getLongitude()); + if (Math.abs(x - ex) <= compare && Math.abs(y - ey) <= compare) { + compare = radius; + am.add(n); + } + } + } catch (IndexOutOfBoundsException e) { + // that's really rare case, but is much efficient than introduce synchronized block + } + } + } + + @Override + public void initLayer(OsmandMapTileMiniView view) { + this.view = view; + //mapTextLayer = view.getLayerByClass(MapTextLayer.class); + } + + public int getRadiusPoi(RotatedTileBox tb) { + int r; + final double zoom = tb.getZoom(); + if (zoom < startZoom) { + r = 0; + } else if (zoom <= 15) { + r = 10; + } else if (zoom <= 16) { + r = 14; + } else if (zoom <= 17) { + r = 16; + } else { + r = 18; + } + + return (int) (r * view.getScaleCoefficient()); + } + + @Override + public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + Set selectedPoiFilters = app.getPoiFilters().getSelectedPoiFilters(); + if (this.filters != selectedPoiFilters) { + this.filters = selectedPoiFilters; + data.clearCache(); + } + + List objects = Collections.emptyList(); + List fullObjects = new ArrayList<>(); + List fullObjectsLatLon = new ArrayList<>(); + List smallObjectsLatLon = new ArrayList<>(); + //if (!filters.isEmpty()) { + if (tileBox.getZoom() >= startZoom) { + data.queryNewData(tileBox); + objects = data.getResults(); + if (objects != null) { + float textScale = app.getSettings().TEXT_SCALE.get(); + float iconSize = getIconSize(app) * 1.5f * textScale; + QuadTree boundIntersections = initBoundIntersections(tileBox); + WaypointHelper wph = app.getWaypointHelper(); + PointImageDrawable pointImageDrawable = PointImageDrawable.getOrCreate(app, + ContextCompat.getColor(app, R.color.osmand_orange), true); + pointImageDrawable.setAlpha(0.8f); + for (Amenity o : objects) { + float x = tileBox.getPixXFromLatLon(o.getLocation().getLatitude(), o.getLocation() + .getLongitude()); + float y = tileBox.getPixYFromLatLon(o.getLocation().getLatitude(), o.getLocation() + .getLongitude()); + + if (tileBox.containsPoint(x, y, iconSize)) { + if (intersects(boundIntersections, x, y, iconSize, iconSize) || + (app.getSettings().SHOW_NEARBY_POI.get() && wph.isRouteCalculated() && !wph.isAmenityNoPassed(o))) { + pointImageDrawable.drawSmallPoint(canvas, x, y, textScale); + smallObjectsLatLon.add(new LatLon(o.getLocation().getLatitude(), + o.getLocation().getLongitude())); + } else { + fullObjects.add(o); + fullObjectsLatLon.add(new LatLon(o.getLocation().getLatitude(), + o.getLocation().getLongitude())); + } + } + } + for (Amenity o : fullObjects) { + int x = (int) tileBox.getPixXFromLatLon(o.getLocation().getLatitude(), o.getLocation() + .getLongitude()); + int y = (int) tileBox.getPixYFromLatLon(o.getLocation().getLatitude(), o.getLocation() + .getLongitude()); + if (tileBox.containsPoint(x, y, iconSize)) { + String id = null; + PoiType st = o.getType().getPoiTypeByKeyName(o.getSubType()); + if (st != null) { + if (RenderingIcons.containsSmallIcon(st.getIconKeyName())) { + id = st.getIconKeyName(); + } else if (RenderingIcons.containsSmallIcon(st.getOsmTag() + "_" + st.getOsmValue())) { + id = st.getOsmTag() + "_" + st.getOsmValue(); + } + } + if (id != null) { + pointImageDrawable = PointImageDrawable.getOrCreate(app, + ContextCompat.getColor(app, R.color.osmand_orange), true, + RenderingIcons.getResId(id)); + pointImageDrawable.setAlpha(0.8f); + pointImageDrawable.drawPoint(canvas, x, y, textScale, false); + } + } + } + this.fullObjectsLatLon = fullObjectsLatLon; + this.smallObjectsLatLon = smallObjectsLatLon; + } + } + //} + //mapTextLayer.putData(this, objects); + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + } + + @Override + public void destroyLayer() { + routingHelper.removeListener(this); + } + + @Override + public boolean drawInScreenPixels() { + return true; + } + + public static void showDescriptionDialog(Context ctx, OsmandApplication app, String text, String title) { + showText(ctx, app, text, title); + } + + static int getResIdFromAttribute(final Context ctx, final int attr) { + if (attr == 0) { + return 0; + } + final TypedValue typedvalueattr = new TypedValue(); + ctx.getTheme().resolveAttribute(attr, typedvalueattr, true); + return typedvalueattr.resourceId; + } + + private static void showText(final Context ctx, final OsmandApplication app, final String text, String title) { + final Dialog dialog = new Dialog(ctx, + app.getSettings().isLightContent() ? R.style.OsmandLightTheme : R.style.OsmandDarkTheme); + + LinearLayout ll = new LinearLayout(ctx); + ll.setOrientation(LinearLayout.VERTICAL); + + final Toolbar topBar = new Toolbar(ctx); + topBar.setClickable(true); + Drawable icBack = app.getUIUtilities().getIcon(AndroidUtils.getNavigationIconResId(ctx)); + topBar.setNavigationIcon(icBack); + topBar.setNavigationContentDescription(R.string.access_shared_string_navigate_up); + topBar.setTitle(title); + topBar.setBackgroundColor(ContextCompat.getColor(ctx, getResIdFromAttribute(ctx, R.attr.pstsTabBackground))); + topBar.setTitleTextColor(ContextCompat.getColor(ctx, getResIdFromAttribute(ctx, R.attr.pstsTextColor))); + topBar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + dialog.dismiss(); + } + }); + + final TextView textView = new TextView(ctx); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + int textMargin = dpToPx(app, 10f); + boolean light = app.getSettings().isLightContent(); + textView.setLayoutParams(llTextParams); + textView.setPadding(textMargin, textMargin, textMargin, textMargin); + textView.setTextSize(16); + textView.setTextColor(ContextCompat.getColor(app, light ? R.color.text_color_primary_light : R.color.text_color_primary_dark)); + textView.setAutoLinkMask(Linkify.ALL); + textView.setLinksClickable(true); + textView.setText(text); + + ScrollView scrollView = new ScrollView(ctx); + ll.addView(topBar); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0); + lp.weight = 1; + ll.addView(scrollView, lp); + scrollView.addView(textView); + + dialog.setContentView(ll); + dialog.setCancelable(true); + dialog.show(); + } + + @Override + public PointDescription getObjectName(Object o) { + if (o instanceof Amenity) { + Amenity amenity = (Amenity) o; + String preferredLang = app.getSettings().MAP_PREFERRED_LOCALE.get(); + boolean transliterateNames = app.getSettings().MAP_TRANSLITERATE_NAMES.get(); + + if (amenity.getType().isWiki()) { + if (Algorithms.isEmpty(preferredLang)) { + preferredLang = app.getLanguage(); + } + preferredLang = OsmandPlugin.onGetMapObjectsLocale(amenity, preferredLang); + } + + return new PointDescription(PointDescription.POINT_TYPE_POI, + amenity.getName(preferredLang, transliterateNames)); + } + return null; + } + + @Override + public boolean disableSingleTap() { + return false; + } + + @Override + public boolean disableLongPressOnMap() { + return false; + } + + @Override + public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List objects, boolean unknownLocation) { + if (tileBox.getZoom() >= startZoom) { + getAmenityFromPoint(tileBox, point, objects); + } + } + + @Override + public LatLon getObjectLocation(Object o) { + if (o instanceof Amenity) { + return ((Amenity) o).getLocation(); + } + return null; + } + + @Override + public boolean isObjectClickable(Object o) { + return o instanceof Amenity; + } + + @Override + public boolean runExclusiveAction(Object o, boolean unknownLocation) { + return false; + } + + @Override + public LatLon getTextLocation(Amenity o) { + return o.getLocation(); + } + + @Override + public int getTextShift(Amenity amenity, RotatedTileBox rb) { + int radiusPoi = getRadiusPoi(rb); + if (isPresentInFullObjects(amenity.getLocation())) { + radiusPoi += (getIconSize(app) - app.getResources().getDimensionPixelSize(R.dimen.favorites_icon_size_small)) / 2; + } + return radiusPoi; + } + + @Override + public String getText(Amenity o) { + return o.getName(view.getSettings().MAP_PREFERRED_LOCALE.get(), + view.getSettings().MAP_TRANSLITERATE_NAMES.get()); + } + + @Override + public boolean isTextVisible() { + return app.getSettings().SHOW_POI_LABEL.get(); + } + + @Override + public boolean isFakeBoldText() { + return false; + } + + @Override + public void newRouteIsCalculated(boolean newRoute, ValueHolder showToast) { + } + + @Override + public void routeWasCancelled() { + } + + @Override + public void routeWasFinished() { + } +} + diff --git a/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java b/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java index 260f30fa50..4ecc092cd7 100644 --- a/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java +++ b/OsmAnd/src/net/osmand/plus/views/OsmandMapTileView.java @@ -77,6 +77,7 @@ public class OsmandMapTileView implements IMapDownloaderCallback { protected OsmandSettings settings = null; private CanvasColors canvasColors = null; private Boolean nightMode = null; + public Bitmap currentCanvas = null; private class CanvasColors { int colorDay = MAP_DEFAULT_COLOR; @@ -104,7 +105,6 @@ public class OsmandMapTileView implements IMapDownloaderCallback { protected static final int emptyTileDivisor = 16; - public interface OnTrackBallListener { public boolean onTrackBallEvent(MotionEvent e); } @@ -121,6 +121,10 @@ public class OsmandMapTileView implements IMapDownloaderCallback { public void onDrawOverMap(); } + public interface IMapImageDrawListener { + public void onDraw(RotatedTileBox viewport,Bitmap bmp); + } + protected static final Log LOG = PlatformUtil.getLog(OsmandMapTileView.class); @@ -144,6 +148,8 @@ public class OsmandMapTileView implements IMapDownloaderCallback { private OnTrackBallListener trackBallDelegate; + private IMapImageDrawListener iMapImageDrawListener; + private AccessibilityActionsProvider accessibilityActions; private List layers = new ArrayList<>(); @@ -366,6 +372,14 @@ public class OsmandMapTileView implements IMapDownloaderCallback { return wasZoomInMultiTouch; } + public IMapImageDrawListener getMapImageDrawListener() { + return iMapImageDrawListener; + } + + public void setMapImageDrawListener(IMapImageDrawListener iMapImageDrawListener) { + this.iMapImageDrawListener = iMapImageDrawListener; + } + public boolean mapGestureAllowed(OsmandMapLayer.MapGestureType type) { for (OsmandMapLayer layer : layers) { if (!layer.isMapGestureAllowed(type)) { @@ -517,8 +531,8 @@ public class OsmandMapTileView implements IMapDownloaderCallback { public void restoreMapRatio() { RotatedTileBox box = currentViewport.copy(); - float rx = (float)box.getCenterPixelX() / box.getPixWidth(); - float ry = (float)box.getCenterPixelY() / box.getPixHeight(); + float rx = (float) box.getCenterPixelX() / box.getPixWidth(); + float ry = (float) box.getCenterPixelY() / box.getPixHeight(); if (mapPosition == OsmandSettings.BOTTOM_CONSTANT) { ry -= 0.35; } @@ -574,12 +588,13 @@ public class OsmandMapTileView implements IMapDownloaderCallback { if (!bufferBitmap.isRecycled()) { RectF rct = new RectF(x1, y1, x2, y2); canvas.drawBitmap(bufferBitmap, null, rct, paintImg); + currentCanvas = bufferBitmap; } canvas.rotate(-rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); } } - private void refreshBaseMapInternal(RotatedTileBox tileBox, DrawSettings drawSettings) { + public void refreshBaseMapInternal(RotatedTileBox tileBox, DrawSettings drawSettings) { if (tileBox.getPixHeight() == 0 || tileBox.getPixWidth() == 0) { return; } @@ -614,9 +629,12 @@ public class OsmandMapTileView implements IMapDownloaderCallback { } long end = SystemClock.elapsedRealtime(); additional.calculateFPS(start, end); + if (iMapImageDrawListener != null){ + iMapImageDrawListener.onDraw(tileBox,bufferBitmap); + } } - private void refreshMapInternal(DrawSettings drawSettings) { + public void refreshMapInternal(DrawSettings drawSettings) { if (view == null) { return; } @@ -821,7 +839,7 @@ public class OsmandMapTileView implements IMapDownloaderCallback { // this method could be called in non UI thread public void refreshMap(final boolean updateVectorRendering) { - if (view != null && view.isShown()) { + if (view != null) { boolean nightMode = application.getDaynightHelper().isNightMode(); Boolean currentNightMode = this.nightMode; boolean forceUpdateVectorDrawing = currentNightMode != null && currentNightMode != nightMode; @@ -869,6 +887,20 @@ public class OsmandMapTileView implements IMapDownloaderCallback { return currentViewport; } + public void setCurrentRotatedTileBox(net.osmand.data.RotatedTileBox tileBox) { + float rx = (float) tileBox.getCenterPixelX() / tileBox.getPixWidth(); + float ry = (float) tileBox.getCenterPixelY() / tileBox.getPixHeight(); + if (mapPosition == OsmandSettings.BOTTOM_CONSTANT) { + ry -= 0.35; + } + tileBox.setCenterLocation(rx, ry); + LatLon screenCenter = tileBox.getLatLonFromPixel(tileBox.getPixWidth() / 2f, tileBox.getPixHeight() / 2f); + mapRatioX = 0; + mapRatioY = 0; + setLatLon(screenCenter.getLatitude(), screenCenter.getLongitude()); + currentViewport = tileBox; + } + public float getDensity() { return currentViewport.getDensity(); } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..758de960ec7947253b058ff79c88ce51f3abe08a 100644 GIT binary patch delta 7592 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 delta 7527 zcmZ9R1x#F9_qSnicXulc?yf}(6dk0vyZc~+d$GaY-3n72TD-Wng<{36#a;jXnwxvy zd?z`{^Q_;>$vQhHImzBB)d(F`2+2ZdEN_QzrQqP;Kq=DNDA{KoXCCly$>bke%{kEW zYe{bxkm2A2p|qUrkN`qGvS9UV-f^1{Tmv^lyIF-rb}Woy4YW{nG-ugNX^Pi~mfp=` zPROtLj()Lc)?7ukwK~-5mOJ!-;(e=AnFyVa>VMqFzl40c*SoDc5o*a@b;>~91z+ch ztOsV^1g?v%i+~^28+(z>D4ts}4Nu!KY0@@ic}aOyN0Zg*A@O0ze6fgX4lJ)y8u7V^f&}%mO4uz`^vA6Lw-Qo5uGH1}vU$a+^|3w-WK=eF_MX>O$GLV)wcAcp zh*T6(L7XBKYUMaiM49U?hWO;vi-q?5hn!~l&|9-5j>yVW53HH{!By|ludBatTh1%I zB<5SDlSLkLXE_TG{URmGqsQ8OhUpwH)igC2r{Pupf>5+__=jg$-USeGmSt)gWkvYZ z-kZ4kYsYKeP;19vcOcmWSx| z+-U`!P#CBplekzlnu)n_x@Iuiyc^K#h`e_RucIjtwccf#Yv!rlCk-Ad{T>ugl(U)K zcNe#pomfa^@rF;Nj;3tB|G|i){vJ0%uGh6cD z2FMo4ZF!>UMst*&*CY9-S$Cus(VF%ebF~qH?wXCx#Pzts_0RRM+ zYv5Sj6g+ByZ#7_5Adc?4B8=U)D=~kH5C>uV<*=;qLoh_%)P#|P$_zYvnM4X;V@w{t zs^YP1ookyt9BdnCtmj&-sAR|gdMh&!aS}xz88G+T-@wuH5H>b&HHYMO2Og>Si1B1{ zKh9cyP>h=J)WnrLcY9Tl!|2?1o*i|FZvjWf|hn zJA~1%+!E@OTjXpyEc&CBS#OGiI2^{wB3mELt`oI@mCrNMI)1pa*3dLUp8SAx{&tqRVFF1ZJ{z$z>2gi} zTXE63p5N1T$3t2kHa}RBa`yJ=4-p~SRJ%cdmJ?kwkEC3eW{imSw-;xNUMxcTHA0OP zGd`#=MOT0UpQp>NN+Zf55Z>oyU9Z$e?`GYDyqqkm$9qAXjo#^Jkihuzlu08}ES|vm^XWsf4)4S3EMyPmjw~ano-X zU9?8uhiY_7Z9;jciUT!{C0Ku zM)M%VYP7tV-{aX%1+kEQPPE;irmt&z0_?tCgmy-iiVx!3o&u(fL2|eB zT%0gk7fXpV#$KfR8bs+Ra?S8o?Kgd)ht(V-7(^RgF

>O|5`!Y@WVeHnjeM=H)Z) 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