Static map component (#1115)

* init of static map views

* add placeholder and reload images
This commit is contained in:
Flescio
2023-06-20 17:55:01 +02:00
committed by GitHub
parent ca5dfbe7f0
commit abd8f1078d
11 changed files with 241 additions and 11 deletions

View File

@@ -258,6 +258,7 @@
67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; };
68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; };
6832733838C57A7D3FE8FEB5 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; };
6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; };
68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; };
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; };
69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; };
@@ -305,6 +306,7 @@
7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; };
7ABAB3A1D52B86FACF2F74CF /* MapTilerGeoCodingServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23B3FAD8B23C421BC0D1B1E /* MapTilerGeoCodingServiceProtocol.swift */; };
7AEC56ADEFC5A7198A17412F /* InviteUsersScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB35E2DB4EFE8E6F3959629 /* InviteUsersScreenUITests.swift */; };
7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; };
7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; };
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; };
7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; };
@@ -575,6 +577,7 @@
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; };
D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; };
D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; };
D6661A94DBD97658B2ADBD6A /* MapTilerStaticMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */; };
D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; };
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; };
D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; };
@@ -601,6 +604,7 @@
E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */; };
E290C78E7F09F47FD2662986 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; };
E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; };
E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */; };
E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */; };
E3291AD16D7A5CB14781819C /* UserNotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */; };
E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; };
@@ -801,6 +805,7 @@
196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = "<group>"; };
1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = "<group>"; };
1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = "<group>"; };
1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMap.swift; sourceTree = "<group>"; };
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyMock.swift; sourceTree = "<group>"; };
1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = "<group>"; };
1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = "<group>"; };
@@ -814,6 +819,7 @@
1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = "<group>"; };
1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItemContent.swift; sourceTree = "<group>"; };
201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLoaderProtocol.swift; sourceTree = "<group>"; };
20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMapProtocol.swift; sourceTree = "<group>"; };
2141693488CE5446BB391964 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItemContent.swift; sourceTree = "<group>"; };
218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = "<group>"; };
@@ -950,6 +956,7 @@
57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = "<group>"; };
584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelTests.swift; sourceTree = "<group>"; };
592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreStaticMapView.swift; sourceTree = "<group>"; };
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -1161,6 +1168,7 @@
B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenCoordinator.swift; sourceTree = "<group>"; };
B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = "<group>"; };
B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = "<group>"; };
B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = "<group>"; };
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; };
@@ -2978,11 +2986,15 @@
isa = PBXGroup;
children = (
AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */,
B48B7AD4908C5C374517B892 /* MapAssets.xcassets */,
622D09D4ECE759189009AEAF /* MapLibreMapView.swift */,
B81B6170DB690013CEB646F4 /* MapLibreModels.swift */,
592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */,
E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */,
33720F7AD25E85E4A84669E8 /* MapTilerGeocoding.swift */,
C23B3FAD8B23C421BC0D1B1E /* MapTilerGeoCodingServiceProtocol.swift */,
1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */,
20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */,
B7AE92E7BFF71797BDE1D261 /* MapTilerStyleBuilder.swift */,
225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */,
);
@@ -3572,6 +3584,7 @@
AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */,
5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */,
0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */,
6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */,
CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */,
2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */,
CCAA0671B46EAFD0BB528E2C /* apple_emojis_data.json in Resources */,
@@ -4033,10 +4046,13 @@
B66757D0254843162595B25D /* MXLogger.swift in Sources */,
C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */,
C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */,
E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */,
D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */,
FCDA202B246F75BA28E10C5F /* MapTilerAuthorization.swift in Sources */,
7ABAB3A1D52B86FACF2F74CF /* MapTilerGeoCodingServiceProtocol.swift in Sources */,
516534FC5C893D57F169D5A8 /* MapTilerGeocoding.swift in Sources */,
D6661A94DBD97658B2ADBD6A /* MapTilerStaticMap.swift in Sources */,
7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */,
14343C2F9AD2BFEA92CA28FF /* MapTilerStyleBuilder.swift in Sources */,
BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */,
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */,

View File

@@ -173,17 +173,13 @@ final class AppSettings {
// MARK: - Maps
// maptiler dynamic map urls
let lightTileMapStyleURL = "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/style.json"
let darkTileMapStyleURL = "https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/style.json"
// maptiler map urls
let lightTileMapStyleURL = "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810"
let darkTileMapStyleURL = "https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3"
// maptiler api key
let mapTilerApiKey = "fU3vlMsMn4Jb6dnEIFsx"
// maptiler static map urls
let lightTileStaticMapStyleURL = "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/{z}/{x}/{y}.png"
let darkTileStaticMapStyleURL = "https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/{z}/{x}/{y}.png"
// maptiler geocoding url
let geocodingURLFormatString = "https://api.maptiler.com/geocoding/%f,%f.json"

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "lightMapBlurred.pdf",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "DarkMapBlurred.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,115 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreLocation
import SwiftUI
struct MapLibreStaticMapView<PinAnnotation: View>: View {
private let coordinates: CLLocationCoordinate2D
private let zoomLevel: Double
private let mapTilerStatic: MapTilerStaticMapProtocol
private let pinAnnotationView: PinAnnotation
@Environment(\.colorScheme) private var colorScheme
@ScaledMetric private var height: CGFloat
@ScaledMetric private var width: CGFloat
@State private var attempt = 0
init(coordinates: CLLocationCoordinate2D, zoomLevel: Double, mapTilerStatic: MapTilerStaticMapProtocol, height: CGFloat, width: CGFloat, @ViewBuilder pinAnnotationView: () -> PinAnnotation) {
self.coordinates = coordinates
self.zoomLevel = zoomLevel
self.mapTilerStatic = mapTilerStatic
_height = .init(wrappedValue: height)
_width = .init(wrappedValue: width)
self.pinAnnotationView = pinAnnotationView()
}
var body: some View {
if let url = mapTilerStatic.staticMapURL(for: colorScheme.mapStyle, coordinates: coordinates, zoomLevel: zoomLevel, size: .init(width: width, height: height)) {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
Image("mapBlurred")
case .success(let image):
ZStack {
image
.resizable()
.aspectRatio(contentMode: .fill)
pinAnnotationView
}
case .failure:
errorView
@unknown default:
EmptyView()
}
}
.id(attempt)
.frame(width: width, height: height)
.clipped()
} else {
Image("mapBlurred")
}
}
var errorView: some View {
Button {
attempt += 1
} label: {
ZStack {
Image("mapBlurred")
VStack {
Image(systemName: "arrow.clockwise")
Text(L10n.actionStaticMapLoad)
}
}
}
}
}
private extension ColorScheme {
var mapStyle: MapTilerStyle {
switch self {
case .light:
return .light
case .dark:
return .dark
@unknown default:
return .light
}
}
}
struct MapLibreStaticMapView_Previews: PreviewProvider {
static var previews: some View {
MapLibreStaticMapView(coordinates: CLLocationCoordinate2D(),
zoomLevel: 15,
mapTilerStatic: MapTilerStaticMapMock(),
height: 150, width: 300) {
Image(systemName: "mappin.circle.fill")
.padding(.bottom, 35)
}
}
}
private struct MapTilerStaticMapMock: MapTilerStaticMapProtocol {
func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize) -> URL? {
switch style {
case .light:
return URL(string: "https://www.maptiler.com/img/cloud/home/map5.webp")
case .dark:
return URL(string: "https://www.maptiler.com/img/cloud/home/map6.webp")
}
}
}

View File

@@ -0,0 +1,45 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreLocation
struct MapTilerStaticMap: MapTilerStaticMapProtocol {
private let lightURL: String
private let darkURL: String
private let key: String
init(key: String, lightURL: String, darkURL: String) {
self.lightURL = lightURL
self.darkURL = darkURL
self.key = key
}
func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize) -> URL? {
var path: String
switch style {
case .light:
path = lightURL
case .dark:
path = darkURL
}
path.append(String(format: "/static/%f,%f,%f/%dx%d@2x.png", coordinates.longitude, coordinates.latitude, zoomLevel, Int(size.width), Int(size.height)))
guard let url = URL(string: path) else { return nil }
let authorization = MapTilerAuthorization(key: key)
return authorization.authorizeURL(url)
}
}

View File

@@ -0,0 +1,21 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreLocation
protocol MapTilerStaticMapProtocol {
func staticMapURL(for style: MapTilerStyle, coordinates: CLLocationCoordinate2D, zoomLevel: Double, size: CGSize) -> URL?
}

View File

@@ -17,12 +17,18 @@
import Foundation
struct MapTilerStyleBuilder: MapTilerStyleBuilderProtocol {
let lightURL: String
let darkURL: String
let key: String
private let lightURL: String
private let darkURL: String
private let key: String
init(lightURL: String, darkURL: String, key: String) {
self.lightURL = lightURL
self.darkURL = darkURL
self.key = key
}
func dynamicMapURL(for style: MapTilerStyle) -> URL? {
let path: String
var path: String
switch style {
case .light:
path = lightURL
@@ -30,6 +36,8 @@ struct MapTilerStyleBuilder: MapTilerStyleBuilderProtocol {
path = darkURL
}
path.append("/style.json")
guard let url = URL(string: path) else { return nil }
let authorization = MapTilerAuthorization(key: key)
return authorization.authorizeURL(url)

1
changelog.d/1115.feature Normal file
View File

@@ -0,0 +1 @@
Add static map url builder and static map UI component with placeholder and reload logic