19 Commits
Author SHA1 Message Date
xiaogg 0ba9547992 fix:优化完善; 2026-04-15 20:03:37 +08:00
xiaogg 5c74c7ccc0 fix:优化完善; 2026-04-13 16:13:23 +08:00
xiaogg 08dcc64ef4 fix:优化完善; 2026-04-01 15:12:54 +08:00
guyongliang 8748a60f7f 优化算路 2026-03-30 16:13:22 +08:00
xiaogg 5843ef6e87 feat:增加途经点,完善规划,导航; 2026-03-27 15:39:36 +08:00
guyongliang 0123025296 优化搜索 2026-03-27 13:23:10 +08:00
guyongliang 587dd48896 调整地址输入 2026-03-27 11:45:07 +08:00
guyongliang 569780ffb4 样式 2026-03-27 09:37:52 +08:00
guyongliang 3aac4e7330 调整地图选点 2026-03-26 18:01:05 +08:00
guyongliang b275c66383 ui 2026-03-26 16:58:57 +08:00
guyongliang bf80931f37 bugfix 2026-03-25 17:56:37 +08:00
guyongliang 67c36f436c bugfix 2026-03-25 17:18:06 +08:00
guyongliang c10c92afdc 增加token 2026-03-25 11:02:15 +08:00
guyongliang d3edf95df3 单独缓存车牌 2026-03-24 09:38:16 +08:00
guyongliang 930953780d 路径接口 2026-03-20 10:23:45 +08:00
guyongliang cd14469d79 调整定位 2026-03-12 13:27:08 +08:00
guyongliang b846d352a2 android 地图 2026-03-10 17:31:27 +08:00
xiaogg a24f41a8d5 fix:添加本地库 2026-03-09 09:38:22 +08:00
xiaogg 65b4a3ac34 接入地图SDK 2026-03-09 08:53:34 +08:00
160 changed files with 9868 additions and 4337 deletions
Vendored
BIN
View File
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(flutter build apk --debug 2>&1 | tail -15)",
"Bash(flutter build apk --debug 2>&1 | tail -10)"
]
}
}
+7 -2
View File
@@ -37,8 +37,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = 7 versionCode = 6
versionName = "1.2.4" versionName = "1.2.3"
} }
signingConfigs { signingConfigs {
@@ -68,3 +68,8 @@ android {
flutter { flutter {
source = "../.." source = "../.."
} }
dependencies {
implementation("com.amap.api:navi-3dmap-location-search:10.0.700_3dmap10.0.700_loc6.4.5_sea9.7.2")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
@@ -1,14 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@@ -17,23 +14,46 @@
<!--定位权限--> <!--定位权限-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <!--用于申请调用A-GPS模块-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"></uses-permission>
<!--如果设置了target >= 28 如果需要启动后台定位则必须声明这个权限-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!--如果您的应用需要后台定位权限,且有可能运行在Android Q设备上,并且设置了target>28,必须增加这个权限声明-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
android:requestLegacyExternalStorage="true" android:label="小羚羚"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/logo" android:icon="@mipmap/logo">
android:label="小羚羚">
<!-- 高德地图Key -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="92495660f7bc990cb475426c47c03b65" />
<!-- 高德地图定位服务 -->
<service android:name="com.amap.api.location.APSService" />
<!--高德导航-->
<activity
android:name="com.amap.api.navi.AmapRouteActivity"
android:theme="@android:style/Theme.NoTitleBar"
android:configChanges="orientation|keyboardHidden|screenSize|navigation" />
<!--自定义导航 Activity-->
<activity
android:name="com.lnkj.ln_jq_app.NavigationActivity"
android:theme="@android:style/Theme.NoTitleBar"
android:configChanges="orientation|keyboardHidden|screenSize|navigation"
android:screenOrientation="portrait" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
@@ -41,7 +61,8 @@
to determine the Window background behind the Flutter UI. --> to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" /> android:resource="@style/NormalTheme"
/>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@@ -54,13 +75,32 @@
android:value="2" /> android:value="2" />
<!-- 请填写你自己的- appKey --> <!-- 请填写你自己的- appKey -->
<meta-data <meta-data android:name="com.alibaba.app.appkey" android:value="335642645"/>
android:name="com.alibaba.app.appkey"
android:value="335642645" />
<!-- 请填写你自己的appSecret --> <!-- 请填写你自己的appSecret -->
<meta-data <meta-data android:name="com.alibaba.app.appsecret" android:value="39628204345a4240b5b645b68a5896c7"/>
android:name="com.alibaba.app.appsecret" <!-- 华为通道的参数appid -->
android:value="39628204345a4240b5b645b68a5896c7" /> <meta-data android:name="com.huawei.hms.client.appid" android:value="" />
<!-- vivo通道的参数api_key为appkey -->
<meta-data android:name="com.vivo.push.api_key" android:value="" />
<meta-data android:name="com.vivo.push.app_id" android:value="" />
<!-- honor通道的参数-->
<meta-data android:name="com.hihonor.push.app_id" android:value="" />
<!-- oppo -->
<meta-data android:name="com.oppo.push.key" android:value="" />
<meta-data android:name="com.oppo.push.secret" android:value="" />
<!-- 小米-->
<meta-data android:name="com.xiaomi.push.id" android:value="id=2222222222222222222" />
<meta-data android:name="com.xiaomi.push.key" android:value="id=5555555555555" />
<!-- 魅族-->
<meta-data android:name="com.meizu.push.id" android:value="" />
<meta-data android:name="com.meizu.push.key" android:value="" />
<!-- 接收推送消息 --> <!-- 接收推送消息 -->
<receiver <receiver
@@ -83,7 +123,6 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -1,6 +1,136 @@
package com.lnkj.ln_jq_app; package com.lnkj.ln_jq_app;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity { public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.lnkj.ln_jq_app/map";
private static final String TAG = "MainActivity";
// 权限请求码
private static final int PERMISSION_REQUEST_CODE = 1001;
private NativeMapView mapView;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
// 注册高德地图导航Platform View
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(
"NativeFirstPage",
new NativeMapFactory(this)
);
}
/**
* 获取当前系统版本需要申请的权限列表
*/
private String[] getRequiredPermissions() {
List<String> permissions = new ArrayList<>();
// 定位权限是必须的
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
// 存储权限处理:Android 13 (API 33) 以下才需要申请 legacy 存储权限
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
return permissions.toArray(new String[0]);
}
/**
* 检查并申请权限
*/
private void checkAndRequestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] requiredPermissions = getRequiredPermissions();
List<String> deniedPermissions = new ArrayList<>();
for (String permission : requiredPermissions) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permission);
}
}
if (!deniedPermissions.isEmpty()) {
ActivityCompat.requestPermissions(
this,
deniedPermissions.toArray(new String[0]),
PERMISSION_REQUEST_CODE
);
} else {
Log.d(TAG, "所有必要权限已授予");
if (mapView != null) {
mapView.startLocation();
}
}
} else {
if (mapView != null) {
mapView.startLocation();
}
}
}
private void requestPermissions() {
checkAndRequestPermissions();
}
public void setMapView(NativeMapView mapView) {
this.mapView = mapView;
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mapView != null) {
mapView.dispose();
mapView = null;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
boolean locationGranted = false;
for (int i = 0; i < permissions.length; i++) {
if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i])
&& grantResults[i] == PackageManager.PERMISSION_GRANTED) {
locationGranted = true;
break;
}
}
if (locationGranted) {
if (mapView != null) {
mapView.startLocation();
}
} else {
// 只有在定位权限确实被拒绝时才弹出提示
Toast.makeText(this, "请授予应用定位权限以正常使用地图功能", Toast.LENGTH_LONG).show();
}
}
}
} }
@@ -0,0 +1,37 @@
package com.lnkj.ln_jq_app;
import android.content.Context;
import io.flutter.plugin.common.MessageCodec;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
/**
* 高德地图导航 Platform View Factory
* 对应iOS的NativeViewFactory
*/
public class NativeMapFactory extends PlatformViewFactory {
private static final String VIEW_TYPE_ID = "NativeFirstPage";
private static NativeMapView mapViewInstance = null;
private final Context context;
public NativeMapFactory(Context context) {
super(StandardMessageCodec.INSTANCE);
this.context = context;
}
@Override
public PlatformView create(Context context, int viewId, Object args) {
mapViewInstance = new NativeMapView(context, viewId, args);
return mapViewInstance;
}
/**
* 获取地图实例,供MainActivity使用
*/
public static NativeMapView getMapView() {
return mapViewInstance;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,407 @@
package com.lnkj.ln_jq_app;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.amap.api.maps.AMapException;
import com.amap.api.navi.AMapNavi;
import com.amap.api.navi.AMapNaviListener;
import com.amap.api.navi.AMapNaviView;
import com.amap.api.navi.AMapNaviViewListener;
import com.amap.api.navi.AMapNaviViewOptions;
import com.amap.api.navi.enums.NaviType;
import com.amap.api.navi.enums.PathPlanningStrategy;
import com.amap.api.navi.model.AMapCalcRouteResult;
import com.amap.api.navi.model.AMapCarInfo;
import com.amap.api.navi.model.AMapLaneInfo;
import com.amap.api.navi.model.AMapModelCross;
import com.amap.api.navi.model.AMapNaviCameraInfo;
import com.amap.api.navi.model.AMapNaviCross;
import com.amap.api.navi.model.AMapNaviLocation;
import com.amap.api.navi.model.AMapNaviRouteNotifyData;
import com.amap.api.navi.model.AMapNaviTrafficFacilityInfo;
import com.amap.api.navi.model.AMapServiceAreaInfo;
import com.amap.api.navi.model.AimLessModeCongestionInfo;
import com.amap.api.navi.model.AimLessModeStat;
import com.amap.api.navi.model.NaviInfo;
import com.amap.api.navi.model.NaviPoi;
import java.util.List;
import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
/**
* 导航
*/
public class NavigationActivity extends ComponentActivity implements AMapNaviListener {
private static final String TAG = "NavigationActivity";
private AMapNavi mAMapNavi;
private AMapNaviView naviView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建导航视图
naviView = new AMapNaviView(this);
naviView.onCreate(savedInstanceState);
naviView.setAMapNaviViewListener(new AMapNaviViewListener() {
@Override
public void onNaviSetting() {
}
@Override
public void onNaviCancel() {
finish();
}
@Override
public boolean onNaviBackClick() {
return false;
}
@Override
public void onNaviMapMode(int i) {
}
@Override
public void onNaviTurnClick() {
}
@Override
public void onNextRoadClick() {
}
@Override
public void onScanViewButtonClick() {
}
@Override
public void onLockMap(boolean b) {
}
@Override
public void onNaviViewLoaded() {
}
@Override
public void onMapTypeChanged(int i) {
}
@Override
public void onNaviViewShowMode(int i) {
}
});
AMapNaviViewOptions aMapNaviViewOptions = new AMapNaviViewOptions();
Bitmap carBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.car);
aMapNaviViewOptions.setCarBitmap(carBitmap);
aMapNaviViewOptions.setSettingMenuEnabled(false);
naviView.setViewOptions(aMapNaviViewOptions);
// 创建 AMapNavi 实例
try {
mAMapNavi = AMapNavi.getInstance(this);
mAMapNavi.addAMapNaviListener(this);
} catch (AMapException e) {
Log.e(TAG, "初始化导航失败", e);
Toast.makeText(this, "初始化导航失败", Toast.LENGTH_SHORT).show();
finish();
return;
}
// 添加到容器
FrameLayout container = new FrameLayout(this);
container.addView(naviView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
setContentView(container);
// 获取 Intent 中的参数
processIntent(getIntent());
// 处理返回键
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
finish();
}
});
Log.d(TAG, "NavigationActivity created");
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
processIntent(intent);
}
/**
* 处理 Intent 参数并启动导航
*/
@SuppressWarnings("unchecked")
private void processIntent(Intent intent) {
if (intent == null) {
Log.w(TAG, "Intent is null");
return;
}
try {
// 获取起点、终点和途经点
NaviPoi startNaviPoi = (NaviPoi) intent.getParcelableExtra("startPoi");
NaviPoi endNaviPoi = (NaviPoi) intent.getParcelableExtra("endPoi");
List<NaviPoi> wayPoints = intent.getParcelableArrayListExtra("wayPoints");
if (wayPoints != null) {
Log.d("Navi", "获取到途径点数量: " + wayPoints.size());
}
if (startNaviPoi == null || endNaviPoi == null) {
Log.e(TAG, "Missing start or end point");
return;
}
// 设置车辆信息
AMapCarInfo carInfo = (AMapCarInfo) intent.getParcelableExtra("carInfo");
if (carInfo != null) {
mAMapNavi.setCarInfo(carInfo);
}
// 计算并启动导航
mAMapNavi.calculateDriveRoute(startNaviPoi, endNaviPoi, wayPoints, PathPlanningStrategy.DRIVING_MULTIPLE_ROUTES_DEFAULT);
Log.d(TAG, "Navigation started from: " + startNaviPoi.getName() + " - " + endNaviPoi.getName());
} catch (Exception e) {
Log.e(TAG, "Process intent error", e);
Toast.makeText(this, "导航启动失败", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onResume() {
super.onResume();
if (naviView != null) {
naviView.onResume();
}
}
@Override
protected void onPause() {
super.onPause();
if (naviView != null) {
naviView.onPause();
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (naviView != null) {
naviView.onSaveInstanceState(outState);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 停止导航
if (mAMapNavi != null) {
mAMapNavi.stopNavi();
mAMapNavi.removeAMapNaviListener(this);
}
if (naviView != null) {
naviView.onDestroy();
}
Log.d(TAG, "NavigationActivity destroyed");
}
private int dp2px(float dp) {
float density = getResources().getDisplayMetrics().density;
return (int) (dp * density + 0.5f);
}
// ============ AMapNaviListener 回调实现 ============
@Override
public void onInitNaviFailure() {
Log.e(TAG, "onInitNaviFailure");
}
@Override
public void onInitNaviSuccess() {
Log.d(TAG, "onInitNaviSuccess");
}
@Override
public void onStartNavi(int i) {
Log.d(TAG, "onStartNavi: " + i);
}
@Override
public void onTrafficStatusUpdate() {
}
@Override
public void onLocationChange(AMapNaviLocation aMapNaviLocation) {
}
@Override
public void onGetNavigationText(int i, String s) {
}
@Override
public void onGetNavigationText(String s) {
}
@Override
public void onEndEmulatorNavi() {
}
@Override
public void onArriveDestination() {
Log.d(TAG, "onArriveDestination");
}
@Override
public void onCalculateRouteFailure(int i) {
Log.e(TAG, "onCalculateRouteFailure: " + i);
Toast.makeText(this, "路径计算失败: " + i, Toast.LENGTH_SHORT).show();
}
@Override
public void updateCameraInfo(AMapNaviCameraInfo[] aMapNaviCameraInfos) {
}
@Override
public void updateIntervalCameraInfo(AMapNaviCameraInfo aMapNaviCameraInfo, AMapNaviCameraInfo aMapNaviCameraInfo1, int i) {
}
@Override
public void onServiceAreaUpdate(AMapServiceAreaInfo[] aMapServiceAreaInfos) {
}
@Override
public void showCross(AMapNaviCross aMapNaviCross) {
}
@Override
public void hideCross() {
}
@Override
public void showModeCross(AMapModelCross aMapModelCross) {
}
@Override
public void hideModeCross() {
}
@Override
public void showLaneInfo(AMapLaneInfo[] aMapLaneInfos, byte[] bytes, byte[] bytes1) {
}
@Override
public void showLaneInfo(AMapLaneInfo aMapLaneInfo) {
}
@Override
public void hideLaneInfo() {
}
@Override
public void onCalculateRouteSuccess(int[] ints) {
Log.d(TAG, "onCalculateRouteSuccess");
if (mAMapNavi != null) {
mAMapNavi.startNavi(NaviType.GPS);
}
}
@Override
public void notifyParallelRoad(int i) {
}
@Override
public void OnUpdateTrafficFacility(AMapNaviTrafficFacilityInfo[] aMapNaviTrafficFacilityInfos) {
}
@Override
public void OnUpdateTrafficFacility(AMapNaviTrafficFacilityInfo aMapNaviTrafficFacilityInfo) {
}
@Override
public void updateAimlessModeStatistics(AimLessModeStat aimLessModeStat) {
}
@Override
public void updateAimlessModeCongestionInfo(AimLessModeCongestionInfo aimLessModeCongestionInfo) {
}
@Override
public void onPlayRing(int i) {
}
@Override
public void onCalculateRouteSuccess(AMapCalcRouteResult aMapCalcRouteResult) {
Log.d(TAG, "onCalculateRouteSuccess (result)");
if (mAMapNavi != null) {
mAMapNavi.startNavi(NaviType.GPS);
}
}
@Override
public void onCalculateRouteFailure(AMapCalcRouteResult aMapCalcRouteResult) {
Log.e(TAG, "onCalculateRouteFailure: " + aMapCalcRouteResult.getErrorCode());
Toast.makeText(this, "路径计算失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onNaviRouteNotify(AMapNaviRouteNotifyData aMapNaviRouteNotifyData) {
}
@Override
public void onGpsSignalWeak(boolean b) {
}
@Override
public void onReCalculateRouteForYaw() {
Log.d(TAG, "onReCalculateRouteForYaw");
}
@Override
public void onReCalculateRouteForTrafficJam() {
}
@Override
public void onArrivedWayPoint(int i) {
}
@Override
public void onGpsOpenStatus(boolean b) {
}
@Override
public void onNaviInfoUpdate(NaviInfo naviInfo) {
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+48 -123
View File
@@ -21,9 +21,11 @@
.amap-callamap, .amap-callamap,
.amap-lib-driving-callBtn, .amap-lib-driving-callBtn,
.amap-copyright, .amap-copyright,
.amap-logo{bottom: 60px} .amap-logo {
display: none !important;
}
/* 去除高德默认的 label 边框 and 背景 */ /* 去除高德默认的 label 边框背景 */
.amap-marker-label { .amap-marker-label {
border: none !important; border: none !important;
background-color: transparent !important; background-color: transparent !important;
@@ -107,7 +109,7 @@
/* --- 导航结果面板 (底部弹出) --- */ /* --- 导航结果面板 (底部弹出) --- */
#panel { #panel {
position: fixed; position: fixed;
bottom: 95px; bottom: 75px;
left: 0; left: 0;
width: 100%; width: 100%;
height: 35%; height: 35%;
@@ -127,7 +129,7 @@
#location-btn { #location-btn {
position: fixed; position: fixed;
right: 10px; right: 10px;
bottom: 105px; bottom: 75px;
/* 默认位置 */ /* 默认位置 */
width: 44px; width: 44px;
height: 44px; height: 44px;
@@ -157,7 +159,7 @@
/* --- 调整比例尺位置 --- */ /* --- 调整比例尺位置 --- */
.amap-scalecontrol { .amap-scalecontrol {
/* 初始状态:避开底部的定位按钮或留出安全间距 */ /* 初始状态:避开底部的定位按钮或留出安全间距 */
bottom: 110px !important; bottom: 80px !important;
left: 10px !important; left: 10px !important;
transition: bottom 0.3s ease; transition: bottom 0.3s ease;
/* 增加平滑动画 */ /* 增加平滑动画 */
@@ -193,10 +195,10 @@
<div id="search-box"> <div id="search-box">
<div class="input-row"> <div class="input-row">
<input id="startInput" placeholder="起点: 请输入当前地点" onfocus="this.select()" /> <input id="startInput" placeholder="起点: 请输入当前地点" />
</div> </div>
<div class="input-row"> <div class="input-row">
<input id="endInput" placeholder="终点: 请输入目的地" onfocus="this.select()" /> <input id="endInput" placeholder="终点: 请输入目的地" />
<button onclick="startRouteSearch()">路径规划</button> <button onclick="startRouteSearch()">路径规划</button>
</div> </div>
</div> </div>
@@ -219,7 +221,6 @@
var currentLat, currentLng; var currentLat, currentLng;
var isTruckMode = false; var isTruckMode = false;
var isInitialLocationSet = false; var isInitialLocationSet = false;
var stationMarkers = []; // 存储所有站点的标记
function initMap() { function initMap() {
@@ -242,11 +243,6 @@
} }
}); });
// 点击地图空白处重置状态
map.on('click', function() {
resetSearchState();
});
// 添加基础控件 // 添加基础控件
map.addControl(new AMap.Scale()); map.addControl(new AMap.Scale());
map.addControl(new AMap.ToolBar({ map.addControl(new AMap.ToolBar({
@@ -288,19 +284,6 @@
}); });
} }
/**
* 重置搜索状态,隐藏面板和路线
*/
function resetSearchState() {
if (document.body.classList.contains('panel-active')) {
console.log("JS->: 重置地图状态");
document.body.classList.remove('panel-active');
var panel = document.getElementById('panel');
panel.style.display = 'none';
if (driving) driving.clear();
}
}
/** /**
* 核心功能 1: 接收 Flutter 传来的定位数据 * 核心功能 1: 接收 Flutter 传来的定位数据
* Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)") * Flutter 端调用: webViewController.evaluateJavascript("updateMyLocation(...)")
@@ -353,8 +336,6 @@
fetchStationInfo(addressComponent.province, addressComponent.city, fetchStationInfo(addressComponent.province, addressComponent.city,
addressComponent.district, lat, lng); addressComponent.district, lat, lng);
fetchStationInfoList(lat, lng);
// 策略1: 优先使用最近的、类型合适的POI的名称 // 策略1: 优先使用最近的、类型合适的POI的名称
if (pois && pois.length > 0) { if (pois && pois.length > 0) {
// 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名 // 查找第一个类型不是“商务住宅”或“地名地址信息”的POI,这类POI通常是具体的建筑或地点名
@@ -416,6 +397,7 @@
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
// "asoco-token": "e28eada8-4611-4dc2-a942-0122e52f52da"
}, },
body: JSON.stringify({ body: JSON.stringify({
province: province, province: province,
@@ -455,79 +437,6 @@
.catch(err => console.error('JS->:获取站点信息失败:', err)); .catch(err => console.error('JS->:获取站点信息失败:', err));
} }
/**
* 获取站点列表
*/
function fetchStationInfoList(lat, lng) {
fetch('https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
longitude: lng,
latitude: lat,
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json(); // 解析 JSON
})
.then(res => {
console.log("JS->:2 接口完整返回:", JSON.stringify(res));
if (res.code === 0 && res.data && Array.isArray(res.data)) {
// 1. 清除旧的站点标记
stationMarkers.forEach(m => m.setMap(null));
stationMarkers = [];
// 2. 循环标记所有加氢站
res.data.forEach(station => {
var stationIcon = new AMap.Icon({
size: new AMap.Size(32, 32),
image: 'ic_tag.png',
imageSize: new AMap.Size(32, 32)
});
var sMarker = new AMap.Marker({
map: map,
position: [station.longitude, station.latitude],
icon: stationIcon,
offset: new AMap.Pixel(-16, -32),
title: station.name,
label: {
content: '<div class="custom-bubble">' + station.name +
'</div>',
direction: 'top'
}
});
// 3. 绑定点击事件:选中即为目的地,并开始规划
sMarker.on('click', function () {
var stationName = station.name || "目的地";
document.getElementById('endInput').value = station.address ||
stationName;
// 更新当前的 destMarker
if (destMarker && destMarker !== sMarker) destMarker.setMap(null);
destMarker = sMarker;
// 直接传入坐标对象,避免关键字搜索失败
var loc = new AMap.LngLat(station.longitude, station.latitude);
startRouteSearch(loc);
});
stationMarkers.push(sMarker);
});
} else {
console.log("JS->: 业务报错或无数据:", res.message);
}
})
.catch(err => console.error('JS->:获取站点信息失败:', err));
}
/** /**
* 地理编码并在地图标记终点 * 地理编码并在地图标记终点
*/ */
@@ -538,6 +447,7 @@
if (destMarker) destMarker.setMap(null); if (destMarker) destMarker.setMap(null);
// 2. 创建自定义图标 // 2. 创建自定义图标
// 假设图标大小为 32x32,你可以根据实际图片尺寸调整 Size
var destIcon = new AMap.Icon({ var destIcon = new AMap.Icon({
size: new AMap.Size(32, 32), // 图标尺寸 size: new AMap.Size(32, 32), // 图标尺寸
image: 'ic_tag.png', // 本地图片路径 image: 'ic_tag.png', // 本地图片路径
@@ -549,6 +459,8 @@
map: map, map: map,
position: [longitude, latitude], position: [longitude, latitude],
icon: destIcon, // 使用自定义图标 icon: destIcon, // 使用自定义图标
// 偏移量:如果图标底部中心是尖角,offset 设为宽的一半的负数,高度的负数
// 这样能确保图片的底部尖端指向地图上的精确位置
offset: new AMap.Pixel(-16, -32), offset: new AMap.Pixel(-16, -32),
title: name, title: name,
label: { label: {
@@ -557,7 +469,17 @@
} }
}); });
console.log("JS->: 终点标记已添加", address); // 4. 打印调试信息
console.log("JS->: 终点标记已添加", address, loc.toString());
// 5. 自动调整视野包含起点和终点
// if (marker) {
// // 如果起点标志已存在,缩放地图以展示两者
// map.setFitView([marker, destMarker], false, [60, 60, 60, 60]);
// } else {
// // 如果没有起点,直接跳到终点
// map.setCenter(loc);
// }
} }
/** /**
@@ -576,12 +498,11 @@
} }
} }
/** /**
* 路径规划 * 路径规划
* @param {AMap.LngLat} [destLoc] 可选的终点坐标
*/ */
function startRouteSearch(destLoc) { function startRouteSearch() {
// 获取输入框的文字
var startKw = document.getElementById('startInput').value; var startKw = document.getElementById('startInput').value;
var endKw = document.getElementById('endInput').value; var endKw = document.getElementById('endInput').value;
@@ -589,59 +510,63 @@
alert("请输入起点"); alert("请输入起点");
return; return;
} }
if (!endKw) { if (!endKw) {
alert("请输入终点"); alert("请输入终点");
return; return;
} }
// 清除旧路线
if (driving) driving.clear(); if (driving) driving.clear();
// 收起键盘
document.getElementById('startInput').blur(); document.getElementById('startInput').blur();
document.getElementById('endInput').blur(); document.getElementById('endInput').blur();
// --- 构造路径规划的点 (使用数组方式,更灵活) ---
var points = []; var points = [];
// 1. 起点逻辑 // 1. 处理起点逻辑
if (!startKw || startKw === '我的位置' || startKw.includes('当前位置')) { // 如果输入框是空的,或者写着 "我的位置",则使用 GPS 坐标
if (!startKw || startKw === '我的位置') {
if (!currentLng || !currentLat) { if (!currentLng || !currentLat) {
if (window.flutter_inappwebview) window.flutter_inappwebview.callHandler('requestLocation'); // 如果还没获取到定位
if (window.flutter_inappwebview) {
window.flutter_inappwebview.callHandler('requestLocation');
}
alert("正在获取定位,请稍后..."); alert("正在获取定位,请稍后...");
return; return;
} }
// 使用精准坐标对象 (避免高德去猜 '我的位置' 关键词)
points.push({ points.push({
keyword: '我的位置', keyword: '我的位置', // 用于显示的名字
location: new AMap.LngLat(currentLng, currentLat) location: new AMap.LngLat(currentLng, currentLat) // 实际导航用的坐标
}); });
} else { } else {
// 如果用户手动输入了地点 (例如 "北京南站")
// 直接存入关键词,让高德自己去搜
points.push({ points.push({
keyword: startKw keyword: startKw
}); });
} }
// 2. 终点逻辑:如果有传入坐标,则直接使用坐标导航,成功率最高 // 2. 处理终点逻辑 (通常是关键词)
if (destLoc) {
points.push({
keyword: endKw,
location: destLoc // 关键:使用精确坐标
});
} else {
points.push({ points.push({
keyword: endKw keyword: endKw
}); });
}
// 3. 发起搜索 // 3. 发起搜索
// points 数组里现在是一个起点对象和一个终点对象
driving.search(points, function (status, result) { driving.search(points, function (status, result) {
if (status === 'complete') { if (status === 'complete') {
console.log('JS: 规划成功'); console.log('JS: 规划成功');
var panel = document.getElementById('panel'); var panel = document.getElementById('panel');
panel.style.display = 'block'; panel.style.display = 'block';
document.body.classList.add('panel-active'); document.body.classList.add('panel-active');
} else {
console.log('JS: 规划失败', result);
alert("规划失败,请检查起终点名称");
} }
// else {
// console.error('JS: 规划失败', result);
// // 如果坐标规划都失败了,通常是由于起终点距离过近或政策限制(如货车禁行)
// alert("路径规划未成功,请尝试微调起终点");
// }
}); });
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 B

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB

After

Width:  |  Height:  |  Size: 112 KiB

@@ -0,0 +1,51 @@
#
# Be sure to run `pod lib lint AMapNavIOSSDK.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'AMapNavIOSSDK'
s.version = '0.1.0'
s.summary = 'A short description of AMapNavIOSSDK.'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/xiaoshuai/AMapNavIOSSDK'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'xiaoshuai' => 'xiaoshuai@net.cn' }
s.source = { :git => 'https://github.com/xiaoshuai/AMapNavIOSSDK.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '12.0'
s.source_files = 'AMapNavIOSSDK/Classes/**/*'
s.resource = 'AMapNavIOSSDK/**/*.bundle'
s.resource_bundles = {
'AMapNavIOSSDKPrivacyInfo' => ['AMapNavIOSSDK/**/PrivacyInfo.xcprivacy']
}
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
s.dependency 'Masonry'
s.dependency 'MJExtension'
s.dependency 'AMapNavi-NO-IDFA'
s.dependency 'AMapLocation-NO-IDFA'
s.dependency 'AMapSearch-NO-IDFA'
s.dependency 'MBProgressHUD'
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>85F4.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
</dict>
</plist>
@@ -0,0 +1,30 @@
//
// AAddHPopView.h
// AMapNavIOSSDK
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
* 气泡弹框视图
*
* 用法:
* AAddHPopView *pop = [[AAddHPopView alloc] init];
* [pop showInView:self.view sourceView:addHbtn];
*
* - 点击气泡外部自动消失
* - 箭头在右下角45度,指向 sourceView 中心
*/
@interface AAddHPopView : UIView
/// 展示气泡,sourceView 为箭头指向的锚定按钮(坐标系属于 view)
- (void)showInView:(UIView *)view sourceView:(UIView *)sourceView;
/// 手动消失(点击空白处会自动调用)
- (void)dismiss;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,359 @@
//
// AAddHPopView.m
// AMapNavIOSSDK
//
#import "AAddHPopView.h"
#import <Masonry/Masonry.h>
#import "UIColor+ANavMap.h"
// ─── 尺寸常量 ────────────────────────────────────────────────────────────────
static const CGFloat kBubbleWidth = 200.f; // 气泡宽度
static const CGFloat kHeaderHeight = 50.f; // 顶部绿色标题区高度
static const CGFloat kOptionHeight = 60.f; // 每行选项高度
static const CGFloat kArrowWidth = 50.f; // 箭头底边宽(aw = ah*2 使左右45度对称)
static const CGFloat kArrowHeight = 15.f; // 箭头高度
static const CGFloat kBubbleRadius = 15.f; // 气泡圆角半径
static const CGFloat kBubbleGap = 5.f; // 气泡底部与 sourceView 顶部间距
static const CGFloat kScreenPadding = 15.f; // 气泡距屏幕边缘最小距离
// ─── 自定义气泡容器:自绘白色气泡 + 底部箭头 ─────────────────────────────────
@interface _ABubbleContainerView : UIView
/// 箭头中心 X(相对于本 view 坐标系),用于 showInView 精确定位
@property (nonatomic, assign) CGFloat arrowCenterX;
@end
@implementation _ABubbleContainerView
- (instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = [UIColor clearColor];
_arrowCenterX = kBubbleWidth / 2.f;
}
return self;
}
- (void)setArrowCenterX:(CGFloat)arrowCenterX {
_arrowCenterX = arrowCenterX;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGFloat bw = kBubbleWidth; // 160
CGFloat bh = kHeaderHeight + kOptionHeight * 2.f; // 150
CGFloat r = kBubbleRadius; // 10
CGFloat aw = kArrowWidth; // 16 箭头底边宽
CGFloat ah = kArrowHeight; // 10 箭头高度
// 箭头在底部偏右:指向右下45度
// 箭头底边: 从 (arrowX-aw/2, bh) 到 (arrowX, bh)
// 箭头尖端: (arrowX + ah, bh + ah) — 相对于底边中心向右下45度
CGFloat arrowX = bw - 15.f; // 箭头底边右端 X(偏右下)
CGFloat arrowTipX = arrowX + ah; // 箭头尖端 X(向右偏移ah = 45度)
CGFloat arrowTipY = bh + ah; // 箭头尖端 Y(向下偏移ah = 45度)
UIBezierPath *path = [UIBezierPath bezierPath];
// ── 顶部左圆角 → 顶部右圆角 ──
[path moveToPoint:CGPointMake(r, 0)];
[path addLineToPoint:CGPointMake(bw - r, 0)];
[path addArcWithCenter:CGPointMake(bw - r, r) radius:r
startAngle:-M_PI_2 endAngle:0 clockwise:YES];
// ── 右边 → 右下圆角 ──
[path addLineToPoint:CGPointMake(bw, bh - r)];
[path addArcWithCenter:CGPointMake(bw - r, bh - r) radius:r
startAngle:0 endAngle:M_PI_2 clockwise:YES];
// ── 底边 → 箭头底边右端 ──
[path addLineToPoint:CGPointMake(arrowX, bh)];
// 箭头斜边(45度向右下到尖端)
[path addLineToPoint:CGPointMake(arrowTipX, arrowTipY)];
// 箭头底边左端(垂直向上)
[path addLineToPoint:CGPointMake(arrowX - ah, bh)];
// ── 左下圆角 ──
[path addArcWithCenter:CGPointMake(r, bh - r) radius:r
startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
// ── 左边 → 左上圆角 ──
[path addLineToPoint:CGPointMake(0, r)];
[path addArcWithCenter:CGPointMake(r, r) radius:r
startAngle:M_PI endAngle:-M_PI_2 clockwise:YES];
[path closePath];
// ── 阴影 + 白色填充 ──
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextSetShadowWithColor(ctx, CGSizeMake(0, 3), 10.f,
[[UIColor blackColor] colorWithAlphaComponent:0.15f].CGColor);
[[UIColor whiteColor] setFill];
[path fill];
CGContextRestoreGState(ctx);
[[UIColor whiteColor] setFill];
[path fill];
// 保存箭头尖端坐标用于定位
_arrowTipX = arrowTipX;
_arrowTipY = arrowTipY;
}
// 供外部访问的箭头尖端坐标
static CGFloat _arrowTipX = 0;
static CGFloat _arrowTipY = 0;
@end
// ─── AAddHPopView ────────────────────────────────────────────────────────────
@interface AAddHPopView ()
@property (nonatomic, strong) _ABubbleContainerView *bubbleContainer;
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIView *separatorLine1;
@property (nonatomic, strong) UILabel *option1Label;
@property (nonatomic, strong) UIView *separatorLine2;
@property (nonatomic, strong) UILabel *option2Label;
@end
@implementation AAddHPopView
#pragma mark - Init
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self p_setupUI];
}
return self;
}
#pragma mark - Setup UI
- (void)p_setupUI {
self.backgroundColor = [UIColor clearColor];
// ── 点击空白处(self)消失 ──
UITapGestureRecognizer *bgTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(dismiss)];
bgTap.cancelsTouchesInView = NO;
[self addGestureRecognizer:bgTap];
// ── 气泡容器 ──
[self addSubview:self.bubbleContainer];
// ── header(绿色标题区,仅裁剪上两角) ──
[self.bubbleContainer addSubview:self.headerView];
[self.headerView addSubview:self.titleLabel];
// ── 分隔线 1 ──
[self.bubbleContainer addSubview:self.separatorLine1];
// ── 选项 1 ──
[self.bubbleContainer addSubview:self.option1Label];
// ── 分隔线 2 ──
[self.bubbleContainer addSubview:self.separatorLine2];
// ── 选项 2 ──
[self.bubbleContainer addSubview:self.option2Label];
// ── 阻止气泡内点击冒泡到 bgTap ──
UITapGestureRecognizer *bubbleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(p_bubbleTapped)];
[self.bubbleContainer addGestureRecognizer:bubbleTap];
// ── Masonry 内部约束(基于 bubbleContainer ──
// 子视图宽度统一约束到 kBubbleWidth(160),气泡主体实际宽度
[self.headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.equalTo(self.bubbleContainer);
make.width.mas_equalTo(kBubbleWidth);
make.height.mas_equalTo(kHeaderHeight);
}];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.headerView);
}];
[self.separatorLine1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.headerView.mas_bottom);
make.left.mas_equalTo(@0);
make.width.mas_equalTo(kBubbleWidth);
make.height.mas_equalTo(0.5);
}];
[self.option1Label mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.separatorLine1.mas_bottom);
make.left.mas_equalTo(@0);
make.width.mas_equalTo(kBubbleWidth);
make.height.mas_equalTo(kOptionHeight);
}];
[self.separatorLine2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.option1Label.mas_bottom);
make.left.mas_equalTo(@0);
make.width.mas_equalTo(kBubbleWidth);
make.height.mas_equalTo(0.5);
}];
[self.option2Label mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.separatorLine2.mas_bottom);
make.left.mas_equalTo(@0);
make.width.mas_equalTo(kBubbleWidth);
make.height.mas_equalTo(kOptionHeight);
}];
}
#pragma mark - Show / Dismiss
- (void)showInView:(UIView *)view sourceView:(UIView *)sourceView {
self.frame = view.bounds;
[view addSubview:self];
// ── 计算 sourceView 在 view 坐标系中的 frame ──
CGRect srcFrame = [sourceView convertRect:sourceView.bounds toView:view];
// ── 容器尺寸 ──
CGFloat containerW = kBubbleWidth; // 160
CGFloat containerH = kHeaderHeight + kOptionHeight * 2.f; // 150
CGFloat ah = kArrowHeight; // 16 箭头高度
// ── 箭头尖端坐标(相对于 bubbleContainer 左下角为原点)──
// 与 drawRect 中一致:arrowX = bw - 35, arrowTipX = arrowX + ah
CGFloat arrowX_inContainer = containerW - 25.f; // 箭头底边右端 X
CGFloat arrowTipX_inContainer = arrowX_inContainer + ah; // 箭头尖端 X(右下45度)
CGFloat arrowTipY_inContainer = containerH + ah; // 箭头尖端 Y
// ── 位置计算:气泡在按钮左上方,箭头尖端指向按钮中心 ──
CGFloat srcCenterX = CGRectGetMidX(srcFrame);
CGFloat srcCenterY = CGRectGetMidY(srcFrame);
// 箭头尖端应指向按钮中心
CGFloat containerX = srcCenterX - arrowTipX_inContainer;
containerX = MAX(kScreenPadding, MIN(containerX, view.bounds.size.width - containerW - kScreenPadding));
// 气泡顶部位置
CGFloat containerY = srcCenterY - arrowTipY_inContainer;
containerY = MAX(kScreenPadding, containerY);
self.bubbleContainer.bounds = CGRectMake(0, 0, containerW, containerH + ah);
// 锚点设置在箭头尖端位置(相对于容器),实现箭头精准指向
self.bubbleContainer.layer.anchorPoint = CGPointMake(arrowTipX_inContainer / containerW,
arrowTipY_inContainer / (containerH + ah));
self.bubbleContainer.center = CGPointMake(srcCenterX - 25, srcCenterY - 20);
// ── 入场动画 ──
self.alpha = 0.f;
self.bubbleContainer.transform = CGAffineTransformMakeScale(0.85f, 0.85f);
[UIView animateWithDuration:0.25f
delay:0.f
usingSpringWithDamping:0.72f
initialSpringVelocity:0.3f
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.alpha = 1.f;
self.bubbleContainer.transform = CGAffineTransformIdentity;
} completion:nil];
}
- (void)dismiss {
[UIView animateWithDuration:0.18f
delay:0.f
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.alpha = 0.f;
self.bubbleContainer.transform = CGAffineTransformMakeScale(0.85f, 0.85f);
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
#pragma mark - Private Actions
- (void)p_bubbleTapped {
// 阻止冒泡,气泡内点击不消失
}
#pragma mark - Lazy Load
- (_ABubbleContainerView *)bubbleContainer {
if (!_bubbleContainer) {
_bubbleContainer = [[_ABubbleContainerView alloc] init];
}
return _bubbleContainer;
}
- (UIView *)headerView {
if (!_headerView) {
_headerView = [[UIView alloc] init];
_headerView.backgroundColor = [UIColor hp_colorWithRGBHex:0x1BA855];
if (@available(iOS 11.0, *)) {
_headerView.layer.cornerRadius = kBubbleRadius;
_headerView.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner;
_headerView.clipsToBounds = YES;
}
}
return _headerView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.text = @"加氢规划模式";
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.font = [UIFont boldSystemFontOfSize:15.f];
_titleLabel.textAlignment = NSTextAlignmentCenter;
}
return _titleLabel;
}
- (UIView *)separatorLine1 {
if (!_separatorLine1) {
_separatorLine1 = [[UIView alloc] init];
_separatorLine1.backgroundColor = [UIColor colorWithWhite:0.88f alpha:1.f];
}
return _separatorLine1;
}
- (UILabel *)option1Label {
if (!_option1Label) {
_option1Label = [[UILabel alloc] init];
_option1Label.text = @"送货规划模式";
_option1Label.textColor = [UIColor hp_colorWithRGBHex:0xC9CDD4];
_option1Label.font = [UIFont boldSystemFontOfSize:14.f];
_option1Label.textAlignment = NSTextAlignmentCenter;
}
return _option1Label;
}
- (UIView *)separatorLine2 {
if (!_separatorLine2) {
_separatorLine2 = [[UIView alloc] init];
_separatorLine2.backgroundColor = [UIColor colorWithWhite:0.88f alpha:1.f];
}
return _separatorLine2;
}
- (UILabel *)option2Label {
if (!_option2Label) {
_option2Label = [[UILabel alloc] init];
_option2Label.text = @"成本计算模式";
_option2Label.textColor = [UIColor hp_colorWithRGBHex:0xC9CDD4];
_option2Label.font = [UIFont boldSystemFontOfSize:14.f];
_option2Label.textAlignment = NSTextAlignmentCenter;
}
return _option2Label;
}
@end
@@ -0,0 +1,24 @@
//
// ABaseViewController.h
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import <UIKit/UIKit.h>
#import <Masonry/Masonry.h>
#import "AMapNavCommonUtil.h"
#define kRoutePlanBarHeight (self.navigationController.navigationBar.frame.size.height + UIApplication.sharedApplication.statusBarFrame.size.height + 0)
#define kRoutePlanStatusBarHeight (UIApplication.sharedApplication.statusBarFrame.size.height + 0)
NS_ASSUME_NONNULL_BEGIN
@interface ABaseViewController : UIViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,33 @@
//
// ABaseViewController.m
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import "ABaseViewController.h"
@interface ABaseViewController ()
@end
@implementation ABaseViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
@@ -0,0 +1,17 @@
//
// ACustomAnnotationView.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/17.
//
#import <AMapNaviKit/AMapNaviKit.h>
#import "AMapNavCommonUtil.h"
NS_ASSUME_NONNULL_BEGIN
@interface ACustomAnnotationView : MAAnnotationView
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,177 @@
//
// ACustomAnnotationView.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/17.
//
#import "ACustomAnnotationView.h"
// ─── 布局常量 ─────────────────────────────────────────────────────────────────
static const CGFloat kIconSize = 30; // 图标宽高
static const CGFloat kIconTextGap = 6.0; // 图标与文字间距
static const CGFloat kMaxTextWidth = 100.0; // 文字区域最大宽度
static const CGFloat kMaxTextLines = 2; // 最多行数
static const CGFloat kVPad = 4.0; // 整体上下内边距(用于 centerOffset 微调)
@interface ACustomAnnotationView ()
@property (nonatomic, strong) UIImageView *iconView;
@property (nonatomic, strong) UILabel *titleLabel;
/// 当前是否选中(用于更新图标 & 文字样式)
@property (nonatomic, assign) BOOL isAnnotationSelected;
@end
@implementation ACustomAnnotationView
#pragma mark - Init / Reuse
- (instancetype)initWithAnnotation:(id<MAAnnotation>)annotation
reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
self.backgroundColor = [UIColor clearColor];
// 关闭 MAAnnotationView 自带的 image 渲染,避免干扰
self.image = nil;
[self _buildSubviews];
[self _updateFromAnnotation];
[self setNeedsLayout];
}
return self;
}
- (void)setAnnotation:(id<MAAnnotation>)annotation {
[super setAnnotation:annotation];
[self _updateFromAnnotation];
[self setNeedsLayout];
}
#pragma mark - Build
- (void)_buildSubviews {
// ── 图标 ──────────────────────────────────────────────────
if (!self.iconView) {
UIImageView *iv = [[UIImageView alloc] init];
iv.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:iv];
self.iconView = iv;
}
// ── 文字 ──────────────────────────────────────────────────
if (!self.titleLabel) {
UILabel *lbl = [[UILabel alloc] init];
lbl.numberOfLines = kMaxTextLines;
lbl.lineBreakMode = NSLineBreakByTruncatingTail;
lbl.textAlignment = NSTextAlignmentLeft;
[self addSubview:lbl];
self.titleLabel = lbl;
}
// 初始化为默认(未选中)样式
[self _applyStyle:NO];
}
/// 根据选中状态切换图标 & 文字样式
- (void)_applyStyle:(BOOL)selected {
if (selected) {
self.iconView.image = [AMapNavCommonUtil imageWithName3x:@"station_select_icon"];
self.titleLabel.font = [UIFont boldSystemFontOfSize:14];
self.titleLabel.textColor = [UIColor colorWithWhite:0.12 alpha:1.0];
} else {
self.iconView.image = [AMapNavCommonUtil imageWithName3x:@"station_normal_icon"];
self.titleLabel.font = [UIFont systemFontOfSize:13];
self.titleLabel.textColor = [UIColor colorWithWhite:0.25 alpha:1.0];
}
}
#pragma mark - Data
- (void)_updateFromAnnotation {
NSString *text = nil;
id<MAAnnotation> ann = self.annotation;
if ([ann respondsToSelector:@selector(title)]) {
text = ann.title;
}
if (text.length == 0) {
text = @"加氢站";
}
self.titleLabel.text = text;
}
#pragma mark - SelectionMAAnnotationView 官方选中回调)
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
self.isAnnotationSelected = selected;
[self _applyStyle:selected];
[self setNeedsLayout];
}
#pragma mark - Layout
- (void)layoutSubviews {
[super layoutSubviews];
// 文字最大 100pt、最多 2 行
CGSize textConstraint = CGSizeMake(kMaxTextWidth,
CGFLOAT_MAX);
CGSize textFit = [self.titleLabel sizeThatFits:textConstraint];
// 高度上限:2 行
CGFloat lineH = self.titleLabel.font.lineHeight;
CGFloat maxTextH = lineH * kMaxTextLines
+ self.titleLabel.font.leading * (kMaxTextLines - 1);
CGFloat textW = MIN(textFit.width, kMaxTextWidth);
CGFloat textH = MIN(textFit.height, maxTextH);
CGFloat totalW = kIconSize + kIconTextGap + textW;
CGFloat totalH = MAX(kIconSize, textH) + kVPad * 2;
// 更新自身 bounds
self.bounds = CGRectMake(0, 0, totalW, totalH);
// 图标:垂直居中
CGFloat iconY = (totalH - kIconSize) * 0.5;
self.iconView.frame = CGRectMake(0, iconY, kIconSize, kIconSize);
// 文字:垂直居中
CGFloat textY = (totalH - textH) * 0.5;
self.titleLabel.frame = CGRectMake(kIconSize + 1, textY, textW, textH);
// 锚点:让图标底部对齐地图坐标点
// MAAnnotationView 的 centerOffset 以 (0,0)=中心 为原点
// 默认选中图标底部对准坐标:向上偏移 totalH/2
self.centerOffset = CGPointMake(totalW / 2.0 - kIconSize / 2.0,
-(totalH / 2.0));
}
#pragma mark - Hit Test
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hit = [super hitTest:point withEvent:event];
if (hit == self || [hit isDescendantOfView:self]) {
return self;
}
return hit;
}
#pragma mark - Touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
id<MAAnnotation> annotation = self.annotation;
if (!annotation) return;
UIView *sv = self.superview;
while (sv && ![sv isKindOfClass:[MAMapView class]]) {
sv = sv.superview;
}
MAMapView *mapView = (MAMapView *)sv;
if (!mapView) return;
// [mapView deselectAnnotation:annotation animated:NO];
}
@end
@@ -0,0 +1,16 @@
//
// ACustomNaviDriveController.h
// AMapNavIOSSDK
//
// Created by admin on 2026/4/11.
//
#import "ABaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface ACustomNaviDriveController : ABaseViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,22 @@
//
// ACustomNaviDriveController.m
// AMapNavIOSSDK
//
// Created by admin on 2026/4/11.
//
#import "ACustomNaviDriveController.h"
@interface ACustomNaviDriveController ()
@end
@implementation ACustomNaviDriveController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end
@@ -0,0 +1,51 @@
//
// AMapNavSDKHeader.h
// Pods
//
// Created by admin on 2026/2/10.
//
#ifndef AMapNavSDKHeader_h
#define AMapNavSDKHeader_h
//#define kAMapSDKDebugFlag
// iPhone X
#define AMP_iPhoneX (UIApplication.sharedApplication.keyWindow.safeAreaInsets.bottom > 0 ? YES : NO)
// Status bar height.
#define AMP_StatusBarHeight (AMP_iPhoneX ? 44.f : 20.f)
// Navigation bar height.
#define AMP_NavigationBarHeight 44.f
// Tabbar height.
#define AMP_TabbarHeight (AMP_iPhoneX ? (49.f+34.f + 5) : 49.f)
// Tabbar safe bottom margin.
#define AMP_TabbarSafeBottomMargin (AMP_iPhoneX ? 34.f : 0.f)
#pragma mark - url
///获取站点列表
#define kGetStationListUrl @"https://beta-esg.api.lnh2e.com/appointment/station/getNearbyHydrogenStationsByLocation"
///单个站点详情
#define kGetStationDetailtUrl @"https://beta-esg.api.lnh2e.com/appointment/station/getStationInfoByArea"
///获取途经点
/**
请求方式:post 暂时调不通
{
   "longitude":"121.254139",
    "latitude":"31.214628",
    "plateNumber":"浙F32111F",
"hydrogenSiteId":""//加氢站DI }
*/
#define kGetRoutePointtUrl @"https://beta-esg.api.lnh2e.com/appointment/truck/truckRouteAlgorithm"
#import "ANavPointModel.h"
#endif /* AMapNavSDKHeader_h */
@@ -0,0 +1,29 @@
//
// AMapNavSDKManager.h
// Pods
//
// Created by admin on 2026/2/10.
//
#import <Foundation/Foundation.h>
#import "ARoutePlaneController.h"
NS_ASSUME_NONNULL_BEGIN
@interface AMapNavSDKManager : NSObject
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic , strong) NSString * localCity;
@property (nonatomic, copy) NSString * locationAddressDetail;
@property (nonatomic , strong , readonly) UIViewController * targetVC;
+ (instancetype)sharedManager;
- (void)configWithKey:(NSString*)key;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,72 @@
//
// AMapNavSDKManager.m
// Pods
//
// Created by admin on 2026/2/10.
//
#import "AMapNavSDKManager.h"
#import "AMapPrivacyUtility.h"
#import <AMapFoundationKit/AMapFoundationKit.h>
@interface AMapNavSDKManager ()
@property (nonatomic , strong , readwrite) UIViewController * targetVC;
@end
@implementation AMapNavSDKManager
+ (instancetype)sharedManager {
static AMapNavSDKManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[AMapNavSDKManager alloc] init];
});
return manager;
}
- (instancetype)init {
self = [super init];
if (self) {
_targetVC = [ARoutePlaneController new];
}
return self;
}
- (void)configWithKey:(NSString*)key {
if (1) {
/*
* 调用隐私合规处理方法
*/
// [AMapPrivacyUtility handlePrivacyAgreeStatusIn:_targetVC];
//
// 初始化高德导航SDK
[self configureAPIKey:key];
}
}
- (UIViewController *)targetVC {
return _targetVC;
}
#pragma mark - private
- (void)configureAPIKey:(NSString*)key {
if ([key length] == 0)
{
NSString *reason = [NSString stringWithFormat:@"apiKey为空,请检查key是否正确设置。"];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"提示" message:reason delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
[alert show];
}
[AMapServices sharedServices].enableHTTPS = YES;
[AMapServices sharedServices].apiKey = (NSString *)key;
}
@end
@@ -0,0 +1,28 @@
//
// ARoutePlaneController.h
// ANavDemo
//
// Created by admin on 2026/2/5.
//
#import <UIKit/UIKit.h>
#import "ABaseViewController.h"
#import "SelectableOverlay.h"
#import "NaviPointAnnotation.h"
#import <AMapNaviKit/AMapNaviKit.h>
#import "ACustomAnnotationView.h"
#import "AMapNavSDKHeader.h"
#import "ACustomPointAnnotation.h"
#import "ATripCalcResponse.h"
#import "AMapNavCommonUtil.h"
NS_ASSUME_NONNULL_BEGIN
@interface ARoutePlaneController : ABaseViewController<AMapNaviDriveDataRepresentable>
@property (nonatomic , strong) AMapNaviDriveView * driveView;
@end
NS_ASSUME_NONNULL_END
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
//
// ASearchAddressController.h
// ANavDemo
//
// Created by admin on 2026/2/6.
//
#import "ABaseViewController.h"
#import <AMapSearchKit/AMapSearchKit.h>
#import <AMapLocationKit/AMapLocationKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ASearchAddressController : ABaseViewController
@property (nonatomic , copy) void(^selectAddressBlk)(AMapPOI * poi);
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,257 @@
//
// ASearchAddressController.m
// ANavDemo
//
// Created by admin on 2026/2/6.
//
#import "ASearchAddressController.h"
#import "AMapNavSDKManager.h"
#import <AMapFoundationKit/AMapFoundationKit.h>
#import "AMapNavCommonUtil.h"
@interface ASearchAddressController ()<UITextFieldDelegate , AMapSearchDelegate,UITableViewDelegate , UITableViewDataSource>
@property (nonatomic , strong) UITableView *tableView;
@property (nonatomic , strong) UIBarButtonItem *rightItem;
@property (nonatomic ,strong)UIButton * backBtn;
@property (nonatomic , strong) NSMutableArray *dataArr;
@property (nonatomic, strong) UITextField *inputAddressTf;
@property (nonatomic, strong) AMapSearchAPI *search;
@property (nonatomic,assign)NSInteger currPage;
@end
@implementation ASearchAddressController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"选择地点";
self.currPage = 1;
_dataArr = [NSMutableArray array];
[self initSubview];
self.search = [[AMapSearchAPI alloc] init];
self.search.delegate = self;
#ifdef kAMapSDKDebugFlag
self.inputAddressTf.text = @"人民广场";
#endif
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^{
[self.inputAddressTf becomeFirstResponder];
});
}
-(void)initSubview {
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:self.backBtn];
UITextField * inputAddressTf = [[UITextField alloc] init];
inputAddressTf.borderStyle = UITextBorderStyleRoundedRect;
inputAddressTf.placeholder = @"输入地址";
inputAddressTf.returnKeyType = UIReturnKeySearch;
inputAddressTf.tag = 100;
inputAddressTf.delegate = self;
[self.view addSubview:inputAddressTf];
[inputAddressTf mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.view).offset(10);
make.right.mas_equalTo(self.view).offset(-10);
make.top.mas_equalTo(self.view).offset(kRoutePlanBarHeight + 10);
make.height.mas_equalTo(@35);
}];
self.inputAddressTf = inputAddressTf;
UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
[btn setTitle:@"当前位置" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor whiteColor];
btn.layer.borderColor = [UIColor blueColor].CGColor;
btn.layer.borderWidth = 1;
btn.layer.cornerRadius = 5;
btn.titleLabel.font = [UIFont systemFontOfSize:12];
btn.hidden = YES;
[btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[btn addTarget:self action:@selector(searchBtnAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
[btn mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(inputAddressTf.mas_right).offset(12);
make.top.mas_equalTo(inputAddressTf);
make.right.mas_equalTo(self.view).offset(-10);
make.height.mas_equalTo(@30);
make.width.mas_equalTo(70);
}];
[self.view addSubview:self.tableView];
[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(inputAddressTf.mas_bottom).offset(5);
make.left.equalTo(self.view);
make.bottom.equalTo(self.view);
make.centerX.equalTo(self.view);
}];
[self.tableView reloadData];
}
#pragma mark -
- (nonnull UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
AMapPOI * m = self.dataArr[indexPath.row];
cell.textLabel.text = [NSString stringWithFormat:@"%@" , m.name ];
return cell;
}
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArr.count;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
AMapPOI * m = self.dataArr[indexPath.row];
if (self.selectAddressBlk) {
self.selectAddressBlk(m);
}
[self backBtnAction];
// [self.navigationController popViewControllerAnimated:YES];
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == self.dataArr.count - 1 && (self.dataArr.count % 20 == 0)) {
self.currPage = self.currPage + 1;
[self requestAddressWithAddress:self.inputAddressTf.text atPage:self.currPage];
}
}
#pragma mark -
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
}
return _tableView;
}
-(UIButton *)backBtn{
if (!_backBtn) {
_backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
_backBtn.frame = CGRectMake(0, 0, 40, 30);
_backBtn.imageEdgeInsets = UIEdgeInsetsMake(2, -5, 2, 5);
// _backBtn.backgroundColor = UIColor.redColor;
[_backBtn setImage:[AMapNavCommonUtil imageWithName:@"icon_fanhui"] forState:UIControlStateNormal];
[_backBtn addTarget:self action:@selector(backBtnAction) forControlEvents:UIControlEventTouchUpInside];
}
return _backBtn;
}
-(void)backBtnAction {
[self dismissViewControllerAnimated:YES completion:^{
}];
}
#pragma mark - Action
-(void)searchBtnAction {
AMapNavSDKManager * sdk = [AMapNavSDKManager sharedManager];
self.inputAddressTf.text = sdk.locationAddressDetail;
[self startSearchWithAddress:self.inputAddressTf.text];
}
-(void)startSearchWithAddress:(NSString *)addr {
if (!addr) {
return;
}
[self requestAddressWithAddress:addr atPage:1];
}
-(void)requestAddressWithAddress:(NSString *)addr atPage:(NSInteger)page {
AMapPOIKeywordsSearchRequest *request = [[AMapPOIKeywordsSearchRequest alloc] init];
request.keywords = addr;
AMapNavSDKManager * sdk = [AMapNavSDKManager sharedManager];
// request.city = sdk.localCity;
// request.types = @"高等院校";
// request.requireExtension = YES;
request.offset =20;
request.page = page;
/* 搜索SDK 3.2.0 中新增加的功能,只搜索本城市的POI。*/
// request.cityLimit = YES;
// request.requireSubPOIs = YES;
[self.search AMapPOIKeywordsSearch:request];
}
#pragma mark -
/* POI 搜索回调. */
- (void)onPOISearchDone:(AMapPOISearchBaseRequest *)request response:(AMapPOISearchResponse *)response
{
NSArray * pois = response.pois;
if (pois.count == 0)
{
return;
}
//解析response获取POI信息,具体解析见 Demo
[self.dataArr addObjectsFromArray:pois];
[self.tableView reloadData];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
[self.dataArr removeAllObjects];
self.currPage = 1;
[self startSearchWithAddress:textField.text];
return YES;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.inputAddressTf resignFirstResponder];
}
@end
@@ -0,0 +1,71 @@
//
// AStationDetailPopupController.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/22.
//
#import <UIKit/UIKit.h>
#import "ANavPointModel.h"
#import "AMapNavSDKHeader.h"
NS_ASSUME_NONNULL_BEGIN
@class AStationDetailPopupController;
@protocol AStationDetailPopupDelegate <NSObject>
@optional
/// 点击"开始导航"
- (void)stationDetailPopupDidTapStartNavi:(AStationDetailPopupController *)popup;
/// 点击关闭
- (void)stationDetailPopupDidTapClose:(AStationDetailPopupController *)popup;
@end
@interface AStationDetailPopupController : UIViewController
@property (nonatomic, strong, nullable) ANavPointModel *pointModel;
/// 预计加氢费用(元),可由外部传入;若 nil 则隐藏
@property (nonatomic, copy, nullable) NSString *estimatedCost;
/// 预计时间,如 @"15分钟";若 nil 则隐藏
@property (nonatomic, copy, nullable) NSString *estimatedTime;
/// 行驶里程,如 @"23.5公里";若 nil 则隐藏
@property (nonatomic, copy, nullable) NSString *driveDistance;
/// 过路费,如 @"30元";若 nil 则隐藏
@property (nonatomic, copy, nullable) NSString *tollFee;
/// 营业时间,如 @"00:00-24:00";有值显示,无值显示 -
@property (nonatomic, copy, nullable) NSString *businessHours;
/// 站点联系人,如 @"陈凯";有值显示,无值显示 -
@property (nonatomic, copy, nullable) NSString *contactName;
/// 联系方式,如 @"18019187371";有值显示,无值显示 -
@property (nonatomic, copy, nullable) NSString *contactPhone;
/// 加氢价格,如 @"32元/L";有值显示,无值显示 -
@property (nonatomic, copy, nullable) NSString *hydrogenPrice;
@property (nonatomic, weak, nullable) id<AStationDetailPopupDelegate> delegate;
/// 以半透明蒙层方式弹出在目标控制器上
- (void)presentInViewController:(UIViewController *)parentVC;
/// 关闭弹框
- (void)dismiss;
/// 关闭弹框,动画结束后执行 completion(用于关闭后再 present 其他页面)
- (void)dismissWithCompletion:(nullable void(^)(void))completion;
-(void)resetUI;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,654 @@
//
// AStationDetailPopupController.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/22.
//
#import "AStationDetailPopupController.h"
#import "ABaseViewController.h"
#import "AMapNavCommonUtil.h"
#import <Masonry/Masonry.h>
// 主题绿色(开始导航按钮背景)
static inline UIColor *AStationThemeGreen(void) {
return [UIColor colorWithRed:0x1A/255.0 green:0x7C/255.0 blue:0x43/255.0 alpha:1.0];
}
@interface AStationDetailPopupController ()
/// 背景蒙层
@property (nonatomic, strong) UIControl *maskControl;
/// 弹框卡片容器
@property (nonatomic, strong) UIView *cardView;
/// 站点名称
@property (nonatomic, strong) UILabel *stationNameLabel;
/// 预计加氢费用(名称右侧)
@property (nonatomic, strong) UIImageView *costIconView;
@property (nonatomic, strong) UILabel *costLabel;
/// 地址
@property (nonatomic, strong) UILabel *addressLabel;
/// 分割线
@property (nonatomic, strong) UIView *separator;
/// 预计时间行
@property (nonatomic, strong) UIImageView *timeIconView;
@property (nonatomic, strong) UILabel *timeLabel;
/// 行驶里程
@property (nonatomic, strong) UIImageView *distanceIconView;
@property (nonatomic, strong) UILabel *distanceLabel;
/// 过路费
@property (nonatomic, strong) UIImageView *tollIconView;
@property (nonatomic, strong) UILabel *tollLabel;
/// 关闭按钮
@property (nonatomic, strong) UIButton *closeButton;
/// 开始导航按钮
@property (nonatomic, strong) UIButton *startNaviButton;
/// 卡片 bottom constraint(动画用)
@property (nonatomic, strong) MASConstraint *cardBottomConstraint;
// ── 营业时间 ──
@property (nonatomic, strong) UILabel *businessHoursLabel;
// ── 站点联系人 ──
@property (nonatomic, strong) UIImageView *contactPersonIconView;
@property (nonatomic, strong) UILabel *contactPersonLabel;
// ── 加氢价格 ──
@property (nonatomic, strong) UIImageView *priceIconView;
@property (nonatomic, strong) UILabel *priceLabel;
// ── 联系方式 ──
@property (nonatomic, strong) UIImageView *phoneIconView;
@property (nonatomic, strong) UILabel *phoneLabel;
@end
@implementation AStationDetailPopupController
#pragma mark - Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor clearColor];
[self _buildUI];
[self _setupMasonryConstraints];
[self _updateUI];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self _playShowAnimation];
}
#pragma mark - Public
- (void)presentInViewController:(UIViewController *)parentVC {
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;
self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[parentVC presentViewController:self animated:NO completion:nil];
}
- (void)dismiss {
[self dismissWithCompletion:nil];
}
- (void)dismissWithCompletion:(void(^)(void))completion {
[self _playDismissAnimationWithCompletion:^{
[self dismissViewControllerAnimated:NO completion:completion];
}];
}
#pragma mark - Setter Override(弹出前赋值 或 弹出后动态更新)
- (void)setPointModel:(ANavPointModel *)pointModel {
_pointModel = pointModel;
if (self.isViewLoaded) [self _updateUI];
}
- (void)setEstimatedCost:(NSString *)estimatedCost {
_estimatedCost = [estimatedCost copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setEstimatedTime:(NSString *)estimatedTime {
_estimatedTime = [estimatedTime copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setDriveDistance:(NSString *)driveDistance {
_driveDistance = [driveDistance copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setTollFee:(NSString *)tollFee {
_tollFee = [tollFee copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setBusinessHours:(NSString *)businessHours {
_businessHours = [businessHours copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setContactName:(NSString *)contactName {
_contactName = [contactName copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setContactPhone:(NSString *)contactPhone {
_contactPhone = [contactPhone copy];
if (self.isViewLoaded) [self _updateUI];
}
- (void)setHydrogenPrice:(NSString *)hydrogenPrice {
_hydrogenPrice = [hydrogenPrice copy];
if (self.isViewLoaded) [self _updateUI];
}
#pragma mark - Build UI
/**
卡片结构:
┌──────────────────────────────────── [✕] ┐
│ 站点名称(加粗) 预计加氢费用:--元 │ ← 同行,间距20pt
│ 地址(灰色小字,多行) │
│ ──────────────────────────────────────── │
│ [icon] 预计时间:-- 分钟 │
│ [icon] 行驶里程:-- 公里 [icon] 过路费:-- 元 │
│ ╔════════════════════════════════════╗ │
│ ║ 开始导航 ║ │
│ ╚════════════════════════════════════╝ │
│ (距底部30pt) │
└─────────────────────────────────────────┘
*/
- (void)_buildUI {
// ── 蒙层 ──
UIControl *mask = [[UIControl alloc] init];
mask.backgroundColor = [UIColor colorWithWhite:0 alpha:0.35];
[mask addTarget:self action:@selector(_onMaskTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:mask];
self.maskControl = mask;
// ── 卡片 ──
UIView *card = [[UIView alloc] init];
card.backgroundColor = [UIColor whiteColor];
card.layer.cornerRadius = 16;
card.layer.masksToBounds = NO;
card.layer.shadowColor = [UIColor blackColor].CGColor;
card.layer.shadowOpacity = 0.15;
card.layer.shadowRadius = 12;
card.layer.shadowOffset = CGSizeMake(0, -4);
[self.view addSubview:card];
self.cardView = card;
// ── 关闭按钮 ──
UIButton *closeBtn = [UIButton buttonWithType:UIButtonTypeCustom];
// [closeBtn setTitle:@"✕" forState:UIControlStateNormal];
[closeBtn setImage: [AMapNavCommonUtil imageWithName:@"icon_close"] forState:UIControlStateNormal];
[closeBtn setTitleColor:[UIColor colorWithWhite:0.5 alpha:1] forState:UIControlStateNormal];
closeBtn.titleLabel.font = [UIFont systemFontOfSize:16];
[closeBtn addTarget:self action:@selector(_onCloseTapped) forControlEvents:UIControlEventTouchUpInside];
[card addSubview:closeBtn];
self.closeButton = closeBtn;
UIColor * headTextColor = [UIColor hp_colorWithRGBHex:0x1D2129];
UIFont * headTextFont = [UIFont hp_pingFangMedium:14];
// ── 站点名称 ──
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.font = [UIFont hp_pingFangMedium:18];
nameLabel.textColor = headTextColor;
nameLabel.numberOfLines = 2;
// nameLabel.adjustsFontSizeToFitWidth = YES;
nameLabel.minimumScaleFactor = 0.8;
[card addSubview:nameLabel];
self.stationNameLabel = nameLabel;
// ── 预计加氢费用(名称右侧,间距20pt) ──
UILabel *costLabel = [[UILabel alloc] init];
costLabel.font = headTextFont;
costLabel.textColor = headTextColor;
costLabel.numberOfLines = 1;
costLabel.textAlignment = NSTextAlignmentLeft;
// [costLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// [costLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[card addSubview:costLabel];
self.costLabel = costLabel;
// ── 营业时间(站点名称下方,4pt间距,浅灰色小字) ──
UILabel *bizHoursLabel = [[UILabel alloc] init];
bizHoursLabel.font = [UIFont hp_pingFangRegular:14];
bizHoursLabel.textColor = [UIColor hp_colorWithRGBHex:0x1D2129];
bizHoursLabel.numberOfLines = 1;
[card addSubview:bizHoursLabel];
self.businessHoursLabel = bizHoursLabel;
// ── 地址 ──
UILabel *addrLabel = [[UILabel alloc] init];
addrLabel.font = [UIFont hp_pingFangRegular:14];
addrLabel.textColor = [UIColor hp_colorWithRGBHex:0x86909C];
addrLabel.numberOfLines = 2;
[card addSubview:addrLabel];
self.addressLabel = addrLabel;
UIImageView *costIcon = [[UIImageView alloc] init];
costIcon.contentMode = UIViewContentModeScaleAspectFit;
costIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_fuel"];
[card addSubview:costIcon];
self.costIconView = costIcon;
// ── 分割线 ──
UIView *sep = [[UIView alloc] init];
sep.backgroundColor = [UIColor colorWithWhite:0.88 alpha:1];
[card addSubview:sep];
self.separator = sep;
// ── 预计时间图标 ──
UIImageView *timeIcon = [[UIImageView alloc] init];
timeIcon.contentMode = UIViewContentModeScaleAspectFit;
timeIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_time"];
[card addSubview:timeIcon];
self.timeIconView = timeIcon;
// ── 预计时间文字 ──
UILabel *timeLabel = [[UILabel alloc] init];
timeLabel.font = headTextFont;
timeLabel.textColor = headTextColor;
[card addSubview:timeLabel];
self.timeLabel = timeLabel;
// ── 行驶里程图标 ──
UIImageView *distIcon = [[UIImageView alloc] init];
distIcon.contentMode = UIViewContentModeScaleAspectFit;
distIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_mileage"];
[card addSubview:distIcon];
self.distanceIconView = distIcon;
// ── 行驶里程文字 ──
UILabel *distLabel = [[UILabel alloc] init];
distLabel.font = headTextFont;
distLabel.textColor = headTextColor;
[card addSubview:distLabel];
self.distanceLabel = distLabel;
// ── 过路费图标 ──
UIImageView *tollIcon = [[UIImageView alloc] init];
tollIcon.contentMode = UIViewContentModeScaleAspectFit;
tollIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_toll"];
[card addSubview:tollIcon];
self.tollIconView = tollIcon;
// ── 过路费文字 ──
UILabel *tollLabel = [[UILabel alloc] init];
tollLabel.font = headTextFont;
tollLabel.textColor = headTextColor;
[card addSubview:tollLabel];
self.tollLabel = tollLabel;
// ── 站点联系人图标 & 文字 ──
UIImageView *personIcon = [[UIImageView alloc] init];
personIcon.contentMode = UIViewContentModeScaleAspectFit;
personIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_person"];
[card addSubview:personIcon];
self.contactPersonIconView = personIcon;
UILabel *personLabel = [[UILabel alloc] init];
personLabel.font = headTextFont;
personLabel.textColor = headTextColor;
[card addSubview:personLabel];
self.contactPersonLabel = personLabel;
// ── 加氢价格图标 & 文字 ──
UIImageView *priceIcon = [[UIImageView alloc] init];
priceIcon.contentMode = UIViewContentModeScaleAspectFit;
priceIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_price"];
[card addSubview:priceIcon];
self.priceIconView = priceIcon;
UILabel *priceLabel = [[UILabel alloc] init];
priceLabel.font = headTextFont;
priceLabel.textColor = headTextColor;
[card addSubview:priceLabel];
self.priceLabel = priceLabel;
// ── 联系方式图标 & 文字 ──
UIImageView *phoneIcon = [[UIImageView alloc] init];
phoneIcon.contentMode = UIViewContentModeScaleAspectFit;
phoneIcon.image = [AMapNavCommonUtil imageWithName3x:@"ic_phone"];
[card addSubview:phoneIcon];
self.phoneIconView = phoneIcon;
UILabel *phoneLabel = [[UILabel alloc] init];
phoneLabel.font = headTextFont;
phoneLabel.textColor = headTextColor;
[card addSubview:phoneLabel];
self.phoneLabel = phoneLabel;
// ── 开始导航按钮 ──
UIButton *naviBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[naviBtn setTitle:@"开始导航" forState:UIControlStateNormal];
[naviBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
naviBtn.titleLabel.font = [UIFont boldSystemFontOfSize:17];
naviBtn.backgroundColor = AStationThemeGreen();
naviBtn.layer.cornerRadius = 24;
[naviBtn addTarget:self action:@selector(_onStartNaviTapped) forControlEvents:UIControlEventTouchUpInside];
[card addSubview:naviBtn];
self.startNaviButton = naviBtn;
}
#pragma mark - Masonry Constraints
- (void)_setupMasonryConstraints {
UIView *card = self.cardView;
CGFloat iconSize = 16;
// ── 蒙层:铺满父视图 ──
[self.maskControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
// ── 卡片:左右各16,底部距 safeArea 30pt ──
// 初始状态卡片在屏幕下方(动画起点),top 留空,先用 bottom 约束做弹入
[card mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
// 动画起点:卡片顶部 = 父视图底部(完全隐藏在屏幕外)
make.top.equalTo(self.view.mas_bottom).offset(0);
}];
// ── 关闭按钮:右上角 ──
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(card).offset(8);
make.right.equalTo(card).offset(-15);
make.width.height.mas_equalTo(40);
}];
// ── 第一行:站点名称(左,内容自适应)+ 预计加氢费用(紧跟nameLabel右侧20pt ──
// nameLabel:内容有多宽占多宽,宽度上限55%,防止过长把费用挤掉
[self.stationNameLabel setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
[self.stationNameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(card).offset(25);
make.left.equalTo(card).offset(16);
// make.width.lessThanOrEqualTo(card).multipliedBy(0.45);
make.right.equalTo(self.closeButton.mas_left).offset(-12);
}];
// ── 营业时间(站点名称下方,6pt间距) ──
[self.businessHoursLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.stationNameLabel.mas_bottom).offset(10);
make.left.equalTo(card).offset(16);
make.right.equalTo(card).offset(-16);
}];
// ── 地址(营业时间下方,6pt间距) ──
[self.addressLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.businessHoursLabel.mas_bottom).offset(10);
make.left.equalTo(card).offset(16);
make.right.equalTo(card).offset(-16);
}];
self.separator.hidden = YES;
// ── 分割线(地址下方,12pt间距) ──
[self.separator mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.addressLabel.mas_bottom).offset(10);
make.left.equalTo(card).offset(16);
make.right.equalTo(card).offset(-16);
make.height.mas_equalTo(0.5);
}];
CGFloat _offset_y = 18;
// ── 预计时间行(分割线下方,14pt) ──
[self.timeIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.separator.mas_bottom).offset(12);
make.left.equalTo(card).offset(16);
make.width.height.mas_equalTo(iconSize);
}];
[self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.timeIconView);
make.left.equalTo(self.timeIconView.mas_right).offset(6);
// make.right.equalTo(card).offset(-16);
make.height.mas_equalTo(24);
}];
///cost
[self.costIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.timeIconView);
make.left.equalTo(self.timeLabel.mas_right).offset(40);
make.width.height.mas_equalTo(iconSize);
}];
[self.costLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.costIconView);
make.left.equalTo(self.costIconView.mas_right).offset(6);
make.right.lessThanOrEqualTo(card).offset(-10);
}];
// ── 行驶里程 + 过路费行(时间行下方,12pt) ──
[self.distanceIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.timeIconView.mas_bottom).offset(_offset_y);
make.left.equalTo(card).offset(16);
make.width.height.mas_equalTo(iconSize);
}];
[self.distanceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.distanceIconView);
make.left.equalTo(self.distanceIconView.mas_right).offset(6);
// 宽度约为可用区域的一半(另一半留给过路费)
make.width.mas_lessThanOrEqualTo(130);
make.height.mas_equalTo(24);
}];
[self.tollIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.distanceIconView);
make.left.equalTo(self.costIconView.mas_left);
make.width.height.mas_equalTo(iconSize);
}];
[self.tollLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.distanceIconView);
make.left.equalTo(self.tollIconView.mas_right).offset(6);
// make.right.equalTo(card).offset(-16);
make.height.mas_equalTo(24);
}];
// ── 站点联系人行(过路费行下方,14pt) ──
[self.contactPersonIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.distanceIconView.mas_bottom).offset(_offset_y);
make.left.equalTo(card).offset(16);
make.width.height.mas_equalTo(iconSize);
}];
[self.contactPersonLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contactPersonIconView);
make.left.equalTo(self.contactPersonIconView.mas_right).offset(6);
make.height.mas_equalTo(24);
}];
// ── 加氢价格(与站点联系人同行,右侧对齐 costIconView ──
[self.priceIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contactPersonIconView);
make.left.equalTo(self.costIconView.mas_left);
make.width.height.mas_equalTo(iconSize);
}];
[self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.contactPersonIconView);
make.left.equalTo(self.priceIconView.mas_right).offset(6);
make.right.lessThanOrEqualTo(card).offset(-10);
make.height.mas_equalTo(24);
}];
// ── 联系方式行(站点联系人行下方,12pt) ──
[self.phoneIconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contactPersonIconView.mas_bottom).offset(_offset_y);
make.left.equalTo(card).offset(16);
make.width.height.mas_equalTo(iconSize);
}];
[self.phoneLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.phoneIconView);
make.left.equalTo(self.phoneIconView.mas_right).offset(6);
make.height.mas_equalTo(24);
}];
// ── 开始导航按钮(联系方式行下方18pt,距卡片底部 safeArea ──
[self.startNaviButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.phoneIconView.mas_bottom).offset(40);
make.left.equalTo(card).offset(16);
make.right.equalTo(card).offset(-16);
make.height.mas_equalTo(48);
make.bottom.equalTo(card).offset(-AMP_TabbarSafeBottomMargin); // 卡片底部内间距30pt
}];
}
#pragma mark - Data Update
- (void)_updateUI {
self.stationNameLabel.text = (self.pointModel.name.length > 0)
? self.pointModel.name : @"-";
// ── 营业时间 ──
self.businessHoursLabel.text = (self.businessHours.length > 0)
? [NSString stringWithFormat:@"营业时间:%@", self.businessHours]
: @"营业时间:-";
self.costLabel.text = (self.estimatedCost.length > 0)
? [NSString stringWithFormat:@"预计加氢费用:%@元", self.estimatedCost]
: @"预计加氢费用:-";
self.addressLabel.text = (self.pointModel.address.length > 0)
? self.pointModel.address : @"-";
// ── 预计时间(始终显示,无值显示"-- 分钟" ──
self.timeLabel.text = (self.estimatedTime.length > 0)
? [NSString stringWithFormat:@"预计时间:%@分钟", self.estimatedTime]
: @"预计时间:-";
// ── 行驶里程(始终显示,无值显示"-- 公里" ──
self.distanceLabel.text = (self.driveDistance.length > 0)
? [NSString stringWithFormat:@"行驶里程:%@公里", self.driveDistance]
: @"行驶里程:-";
// ── 过路费(始终显示,无值显示"-- 元" ──
self.tollLabel.text = (self.tollFee.length > 0)
? [NSString stringWithFormat:@"过路费:%@元", self.tollFee]
: @"过路费:-";
// ── 站点联系人(有值显示值,无值显示 -) ──
self.contactPersonLabel.text = (self.contactName.length > 0)
? [NSString stringWithFormat:@"站联系人:%@", self.contactName]
: @"站联系人:-";
// ── 加氢价格(有值显示值,无值显示 -) ──
self.priceLabel.text = (self.hydrogenPrice.length > 0)
? [NSString stringWithFormat:@"加氢价格:%@/L", self.hydrogenPrice]
: @"加氢价格:-";
// ── 联系方式(有值显示值,无值显示 -) ──
self.phoneLabel.text = (self.contactPhone.length > 0)
? [NSString stringWithFormat:@"联系方式:%@", self.contactPhone]
: @"联系方式:-";
}
// 重置 UI 状态
-(void)resetUI {
self.stationNameLabel.text = @"-";
self.businessHoursLabel.text = @"营业时间:-";
self.costLabel.text = @"预计加氢费用:-";
self.addressLabel.text = @"地址:-";
self.timeLabel.text = @"预计时间:-";
self.distanceLabel.text = @"行驶里程:-";
self.tollLabel.text = @"过路费:-";
self.contactPersonLabel.text = @"站联系人:-";
self.priceLabel.text = @"加氢价格:-";
self.phoneLabel.text = @"联系方式:-";
}
#pragma mark - Animation
/**
弹入动画:更新 top 约束将卡片滑入至距底部 30pt(含 safeArea
*/
- (void)_playShowAnimation {
// 目标状态:卡片底部紧贴 safeArea 底部,再往上留 30pt
[self.cardView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(0);
make.right.equalTo(self.view).offset(-0);
make.bottom.equalTo(self.view).offset(0);
}];
self.maskControl.alpha = 0;
[UIView animateWithDuration:0.36
delay:0
usingSpringWithDamping:0.82
initialSpringVelocity:0.5
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.maskControl.alpha = 1;
[self.view layoutIfNeeded];
} completion:nil];
}
- (void)_playDismissAnimationWithCompletion:(void(^)(void))completion {
[self.cardView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(16);
make.right.equalTo(self.view).offset(-16);
make.top.equalTo(self.view.mas_bottom).offset(20);
}];
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.maskControl.alpha = 0;
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
if (completion) completion();
}];
}
#pragma mark - Actions
- (void)_onMaskTapped {
if ([self.delegate respondsToSelector:@selector(stationDetailPopupDidTapClose:)]) {
[self.delegate stationDetailPopupDidTapClose:self];
}
[self dismiss];
}
- (void)_onCloseTapped {
if ([self.delegate respondsToSelector:@selector(stationDetailPopupDidTapClose:)]) {
[self.delegate stationDetailPopupDidTapClose:self];
}
[self dismiss];
}
- (void)_onStartNaviTapped {
// 先关闭弹框(等动画完全结束),再通知 delegate 弹出导航页
// 避免两个 presentViewController 同时进行导致导航页面无法显示
__weak typeof(self) weakSelf = self;
[self dismissWithCompletion:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if ([strongSelf.delegate respondsToSelector:@selector(stationDetailPopupDidTapStartNavi:)]) {
[strongSelf.delegate stationDetailPopupDidTapStartNavi:strongSelf];
}
}];
}
@end
@@ -0,0 +1,67 @@
//
// AAlgorithmPathModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
NS_ASSUME_NONNULL_BEGIN
@interface AAlgorithmPathModel : NSObject
/// 单程
@property (nonatomic, copy, nullable) NSString *tripRecommendationId;
@property (nonatomic, copy, nullable) NSString *staId;
@property (nonatomic, copy, nullable) NSString *tripOneWayPathId;
@property (nonatomic, copy, nullable) NSString *tripReturnPathId;
/// 里程(公里)
@property (nonatomic, copy, nullable) NSString *oneWayDis;
@property (nonatomic, copy, nullable) NSString *returnDis;
@property (nonatomic, copy, nullable) NSString *roundTripDis;
/// 时长(分钟)
@property (nonatomic, copy, nullable) NSString *oneWayTime;
@property (nonatomic, copy, nullable) NSString *returnTime;
@property (nonatomic, copy, nullable) NSString *roundTripTime;
/// 总成本(元)
@property (nonatomic, copy, nullable) NSString *oneWayCost;
@property (nonatomic, copy, nullable) NSString *returnCost;
@property (nonatomic, copy, nullable) NSString *roundTripCost;
/// 人工成本(元)
@property (nonatomic, copy, nullable) NSString *oneWayLaborCost;
@property (nonatomic, copy, nullable) NSString *returnLaborCost;
@property (nonatomic, copy, nullable) NSString *roundTripLaborCost;
/// 高速成本(元)
@property (nonatomic, copy, nullable) NSString *oneWayChargerouteCost;
@property (nonatomic, copy, nullable) NSString *returnChargerouteCost;
@property (nonatomic, copy, nullable) NSString *roundTripChargerouteCost;
/// 氢耗(公斤)
@property (nonatomic, copy, nullable) NSString *oneWayHydrogenConsumption;
@property (nonatomic, copy, nullable) NSString *returnLaborHydrogenConsumption;
@property (nonatomic, copy, nullable) NSString *roundTripHydrogenConsumption;
/// 氢气成本(元)
@property (nonatomic, copy, nullable) NSString *oneWayHydrogenCost;
@property (nonatomic, copy, nullable) NSString *returnLaborHydrogenCost;
@property (nonatomic, copy, nullable) NSString *roundTripHydrogenCost;
/// 加氢站相关
@property (nonatomic, copy, nullable) NSString *hydrogenCost; // 氢气总成本(元)
@property (nonatomic, copy, nullable) NSString *hydrogenStaServiceTime; // 站服务总时长(分钟)
@property (nonatomic, copy, nullable) NSString *hydrogenStaRefuelingTime;// 实际加油时长(分钟)
@property (nonatomic, copy, nullable) NSString *hydrogenStaQueueTime; // 排队时长(分钟)
@property (nonatomic, copy, nullable) NSString *hydrogenStaServiceTimeCost; // 站服务时间成本(元)
@property (nonatomic, copy, nullable) NSString *hydrogenStaRefuelingTimeCost;// 加油时间成本(元)
@property (nonatomic, copy, nullable) NSString *hydrogenStaQueueTimeCost; // 排队时间成本(元)
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,12 @@
//
// AAlgorithmPathModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "AAlgorithmPathModel.h"
@implementation AAlgorithmPathModel
@end
@@ -0,0 +1,17 @@
//
// ACustomPointAnnotation.h
// Pods
//
// Created by admin on 2026/3/25.
//
#import <AMapNaviKit/AMapNaviKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ACustomPointAnnotation : MAPointAnnotation
@property (nonatomic, copy, nullable) NSString *stationID;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,12 @@
//
// ACustomPointAnnotation.m
// Pods
//
// Created by admin on 2026/3/25.
//
#import "ACustomPointAnnotation.h"
@implementation ACustomPointAnnotation
@end
@@ -0,0 +1,81 @@
//
// AMapHyStationModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
NS_ASSUME_NONNULL_BEGIN
/**
{
"name": "嘉兴经开站",
"shortName": null,
"siteNo": null,
"city": null,
"address": "嘉兴市秀洲区岗山路272号",
"contact": "龚明伟",
"phone": "18888888888",
"type": null,
"coOpMode": null,
"booking": null,
"siteStatus": 0,
"startBusiness": "06:00:00",
"endBusiness": "22:00:00",
"billingMethod": null,
"term": null,
"remark": null,
"longitude": "120.75972800",
"latitude": "30.79962800"
}
*/
@interface AMapHyStationModel : NSObject
@property (nonatomic, copy, nullable) NSString *ID;
@property (nonatomic, copy, nullable) NSString *hydrogenId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy, nullable) NSString *shortName;
@property (nonatomic, copy, nullable) NSString *siteNo;
@property (nonatomic, copy, nullable) NSString *city;
@property (nonatomic, copy, nullable) NSString *address;
@property (nonatomic, copy, nullable) NSString *contact;
@property (nonatomic, copy, nullable) NSString *phone;
@property (nonatomic, copy, nullable) NSString *type;
@property (nonatomic, copy, nullable) NSString *coOpMode;
@property (nonatomic, strong, nullable) NSString * booking;
@property (nonatomic, assign) NSInteger siteStatus;
@property (nonatomic, copy, nullable) NSString *startBusiness;
@property (nonatomic, copy, nullable) NSString *endBusiness;
@property (nonatomic, copy, nullable) NSString *billingMethod;
@property (nonatomic, copy, nullable) NSString *term;
@property (nonatomic, copy, nullable) NSString *remark;
@property (nonatomic, copy, nullable) NSString *longitude;
@property (nonatomic, copy, nullable) NSString *latitude;
@end
/**
{
"code": 0,
"status": true,
"message": "success",
"data": [],
"time": "1770800256408",
"error": null
}
*/
@interface AMapHyResponse : NSObject
@property (nonatomic, assign) NSInteger code;
@property (nonatomic, assign) NSInteger status;
@property (nonatomic, copy, nullable) NSString *message;
@property (nonatomic, copy, nullable) NSString *time;
@property (nonatomic, copy, nullable) NSString *error;
@property(nonatomic , strong)NSArray <AMapHyStationModel * > * data;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,25 @@
//
// AMapHyStationModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import "AMapHyStationModel.h"
@implementation AMapHyStationModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
return @{ @"ID" : @"id"};
}
@end
@implementation AMapHyResponse
+ (NSDictionary *)mj_objectClassInArray {
return @{@"data" : AMapHyStationModel.class};
}
@end
@@ -0,0 +1,26 @@
//
// ANavPointModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ANavPointModel : NSObject
@property (nonatomic, copy, nullable) NSString *name;
@property (nonatomic, copy, nullable) NSString *address;
@property (nonatomic, copy, nullable) NSString *stationID;
///经纬度
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
///初始化
+ (instancetype)instanceWithCoordinate:(CLLocationCoordinate2D)coordinate name:(NSString *)name address:(NSString *)address;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,22 @@
//
// ANavPointModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "ANavPointModel.h"
@implementation ANavPointModel
///初始化
+ (instancetype)instanceWithCoordinate:(CLLocationCoordinate2D)coordinate name:(NSString *)name address:(NSString *)address {
ANavPointModel *instance = [[ANavPointModel alloc] init];
if (instance) {
instance.coordinate = coordinate;
instance.name = name;
instance.address = address;
}
return instance;
}
@end
@@ -0,0 +1,32 @@
//
// ANaviPathInfoModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
NS_ASSUME_NONNULL_BEGIN
/**
{
"name": "浙江省嘉兴市平湖市乍浦镇滨海大道中国石化滨海大道加油加气站",
"poiId": "",
"coordinate": {
"longitude": "121.070434",
"latitude": "30.596124"
}
}
*/
@interface ANaviPathInfoModel : NSObject
@property (nonatomic, copy, nullable) NSString *name;
@property (nonatomic, copy, nullable) NSString *poiId;
@property (nonatomic, copy, nullable) NSString *longitude;
@property (nonatomic, copy, nullable) NSString *latitude;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,20 @@
//
// ANaviPathInfoModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "ANaviPathInfoModel.h"
@implementation ANaviPathInfoModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
// JSON 中 coordinate 是嵌套对象,展开为 longitude/latitude
return @{
@"longitude": @"coordinate.longitude",
@"latitude": @"coordinate.latitude"
};
}
@end
@@ -0,0 +1,29 @@
//
// APathModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
#import "ANaviPathInfoModel.h"
NS_ASSUME_NONNULL_BEGIN
@class ANaviPathInfoModel;
@interface APathModel : NSObject
@property (nonatomic, assign) CGFloat distance; // 总距离(米)
@property (nonatomic, assign) CGFloat duration; // 总时长(秒)
@property (nonatomic, copy, nullable) NSString *strategy; // 路线策略,如"避免拥堵"
@property (nonatomic, copy, nullable) NSString *tolls; // 高速费(元)
@property (nonatomic, assign) CGFloat toll_distance; // 高速里程(米)
@property (nonatomic, assign) NSInteger restriction; // 限行标志,-1 无
@property (nonatomic, assign) NSInteger traffic_lights; // 红绿灯数量
@property (nonatomic, strong) NSArray<ANaviPathInfoModel *> *naviList; // 途经点列表
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,17 @@
//
// APathModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "APathModel.h"
#import "ANaviPathInfoModel.h"
@implementation APathModel
+ (NSDictionary *)mj_objectClassInArray {
return @{@"naviList": [ANaviPathInfoModel class]};
}
@end
@@ -0,0 +1,69 @@
//
// ASiteModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
NS_ASSUME_NONNULL_BEGIN
/**
{
"innerSiteId": "202304241822210001",
"name": "滨海大道加油加气站",
"shortName": "滨海",
"siteNo": "000001",
"city": "浙江省-嘉兴市",
"address": "嘉兴市平湖市滨海大道1515号",
"contact": "陆平",
"phone": "18666666666",
"type": "",
"coOpMode": "签约",
"booking": "无需预约",
"siteStatus": "0",
"siteStatusName": "营运中",
"startBusiness": "08:00:00",
"endBusiness": "20:00:00",
"billingMethod": "月付款",
"term": "1703952000000",
"remark": "",
"longitude": "121.07112700",
"latitude": "30.59577700",
"distance": ""
}
*/
@interface ASiteModel : NSObject
@property (nonatomic, copy, nullable) NSString *innerSiteId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy, nullable) NSString *shortName;
@property (nonatomic, copy, nullable) NSString *siteNo;
@property (nonatomic, copy, nullable) NSString *city;
@property (nonatomic, copy, nullable) NSString *address;
@property (nonatomic, copy, nullable) NSString *contact;
@property (nonatomic, copy, nullable) NSString *phone;
@property (nonatomic, copy, nullable) NSString *type;
@property (nonatomic, copy, nullable) NSString *coOpMode;
@property (nonatomic, copy, nullable) NSString *booking;
@property (nonatomic, copy, nullable) NSString *siteStatus;
@property (nonatomic, copy, nullable) NSString *siteStatusName;
@property (nonatomic, copy, nullable) NSString *startBusiness;
@property (nonatomic, copy, nullable) NSString *endBusiness;
@property (nonatomic, copy, nullable) NSString *billingMethod;
@property (nonatomic, copy, nullable) NSString *term;
@property (nonatomic, copy, nullable) NSString *remark;
@property (nonatomic, copy, nullable) NSString *longitude;
@property (nonatomic, copy, nullable) NSString *latitude;
@property (nonatomic, copy, nullable) NSString *distance;
///新增弹框参数4.13
@property (nonatomic, copy, nullable) NSString *hydrogenPrice;
@property (nonatomic, copy, nullable) NSString *liaisonName;
@property (nonatomic, copy, nullable) NSString *liaisonPhone;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,12 @@
//
// ASiteModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "ASiteModel.h"
@implementation ASiteModel
@end
@@ -0,0 +1,65 @@
//
// ATripCalcResponse.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
#import "AAlgorithmPathModel.h"
#import "APathModel.h"
#import "ASiteModel.h"
#import "ANaviPathInfoModel.h"
#import "ATruckModel.h"
NS_ASSUME_NONNULL_BEGIN
@class ATruckModel;
@class ASiteModel;
@class APathModel;
@class AAlgorithmPathModel;
#pragma mark - data 节点
/**
{
"truckDto": { ... },
"truckDtoStr": null,
"destinationSite": { ... },
"pathDto": { ... },
"algorithmPath": { ... },
"isInvokeAlgorithm": true
}
*/
@interface ATripCalcDataModel : NSObject
@property (nonatomic, strong, nullable) ATruckModel *truckDto;
@property (nonatomic, copy, nullable) NSString *truckDtoStr;
@property (nonatomic, strong, nullable) ASiteModel *destinationSite;
@property (nonatomic, strong, nullable) APathModel *pathDto;
@property (nonatomic, strong, nullable) AAlgorithmPathModel *algorithmPath;
@property (nonatomic, assign) BOOL isInvokeAlgorithm;
@end
#pragma mark - 根响应
/**
{
"code": 200,
"msg": "操作成功!",
"data": { ... }
}
*/
@interface ATripCalcResponse : NSObject
@property (nonatomic, assign) NSInteger code;
@property (nonatomic, copy, nullable) NSString *msg;
@property (nonatomic, strong, nullable) ATripCalcDataModel *data;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,33 @@
//
// ATripCalcResponse.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "ATripCalcResponse.h"
#import "ATruckModel.h"
#import "ASiteModel.h"
#import "APathModel.h"
#import "AAlgorithmPathModel.h"
@implementation ATripCalcDataModel
+ (NSDictionary *)mj_objectClassInArray {
return @{
@"truckDto": [ATruckModel class],
@"destinationSite": [ASiteModel class],
@"pathDto": [APathModel class],
@"algorithmPath": [AAlgorithmPathModel class]
};
}
@end
@implementation ATripCalcResponse
+ (NSDictionary *)mj_objectClassInArray {
return @{@"data":[ATripCalcDataModel class]};
}
@end
@@ -0,0 +1,58 @@
//
// ATruckModel.h
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import <Foundation/Foundation.h>
#import <MJExtension/MJExtension.h>
NS_ASSUME_NONNULL_BEGIN
/**
{
"isRestriction": true,
"mvehicleSizeName": "重型货车",
"mvehicleAxisUnit": "轴",
"mcarNumber": "浙F32111F",
"mcarType": 0,
"mvehicleHeight": "3.8",
"mvehicleHeightUnit": "M",
"mvehicleWeight": null,
"mvehicleWeightUnit": "T",
"mvehicleLoad": "49.0",
"mvehicleLoadUnit": "T",
"mvehicleLoadSwitch": false,
"mvehicleWidth": "0.0",
"mvehicleWidthUnit": "M",
"mvehicleLength": "7.6",
"mvehicleLengthUnit": "M",
"mvehicleSize": 4,
"mvehicleAxis": 5
}
*/
@interface ATruckModel : NSObject
@property (nonatomic, assign) BOOL isRestriction;
@property (nonatomic, copy, nullable) NSString *mvehicleSizeName;
@property (nonatomic, copy, nullable) NSString *mvehicleAxisUnit;
@property (nonatomic, copy, nullable) NSString *mcarNumber;
@property (nonatomic, assign) NSInteger mcarType;
@property (nonatomic, copy, nullable) NSString *mvehicleHeight;
@property (nonatomic, copy, nullable) NSString *mvehicleHeightUnit;
@property (nonatomic, copy, nullable) NSString *mvehicleWeight;
@property (nonatomic, copy, nullable) NSString *mvehicleWeightUnit;
@property (nonatomic, copy, nullable) NSString *mvehicleLoad;
@property (nonatomic, copy, nullable) NSString *mvehicleLoadUnit;
@property (nonatomic, assign) BOOL mvehicleLoadSwitch;
@property (nonatomic, copy, nullable) NSString *mvehicleWidth;
@property (nonatomic, copy, nullable) NSString *mvehicleWidthUnit;
@property (nonatomic, copy, nullable) NSString *mvehicleLength;
@property (nonatomic, copy, nullable) NSString *mvehicleLengthUnit;
@property (nonatomic, assign) NSInteger mvehicleSize;
@property (nonatomic, assign) NSInteger mvehicleAxis;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,12 @@
//
// ATruckModel.m
// AMapNavIOSSDK
//
// Created by admin on 2026/3/25.
//
#import "ATruckModel.h"
@implementation ATruckModel
@end
@@ -0,0 +1,43 @@
//
// AMapNavCommonUtil.h
// Pods
//
// Created by admin on 2026/2/11.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UIColor+ANavMap.h"
#import "UIFont+HP.h"
NS_ASSUME_NONNULL_BEGIN
@interface AMapNavCommonUtil : NSObject
/// 显示加载转圈(nil 消息时不显示文字)
+ (void)showLoadingWithMsg:(nullable NSString *)msg;
/// 关闭所有 MBProgressHUD
+ (void)dismiss;
/// 显示提示,自动 2.0s 后消失
+ (void)showMsg:(NSString *)msg;
/// 获取图片(2x
+ (UIImage *)imageWithName:(NSString *)name;
/// 获取图片(3x
+ (UIImage *)imageWithName3x:(NSString *)name;
/// 判断字符串是否为空
BOOL stringIsEmpty(NSString *str);
/// 判断字符串是否非空
BOOL stringIsNotEmpty(NSString *str);
+(NSString *)stringValueFromStr:(NSString *)str;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,153 @@
//
// AMapNavCommonUtil.m
// Pods
//
// Created by admin on 2026/2/11.
//
#import "AMapNavCommonUtil.h"
#import <MBProgressHUD/MBProgressHUD.h>
/// 共享 HUD 实例
static MBProgressHUD *_sharedHUD = nil;
@implementation AMapNavCommonUtil
#pragma mark - MBProgressHUD
+ (UIWindow *)_keyWindow {
if (@available(iOS 13.0, *)) {
for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive) {
for (UIWindow *window in scene.windows) {
if (window.isKeyWindow) return window;
}
}
}
}
return [UIApplication sharedApplication].keyWindow;
}
+ (void)showLoadingWithMsg:(NSString *)msg {
dispatch_async(dispatch_get_main_queue(), ^{
// [self dismiss];
UIWindow *window = [self _keyWindow];
if (!window) return;
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:window animated:YES];
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.bezelView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.75];
hud.contentColor = [UIColor whiteColor];
if (msg.length > 0) {
hud.label.text = msg;
hud.label.font = [UIFont systemFontOfSize:14];
}
hud.removeFromSuperViewOnHide = YES;
_sharedHUD = hud;
});
}
+ (void)dismiss {
dispatch_async(dispatch_get_main_queue(), ^{
if (_sharedHUD) {
[_sharedHUD hideAnimated:YES];
_sharedHUD = nil;
} else {
//兜底:隐藏所有 HUD(防止有遗漏)
UIWindow *window = [self _keyWindow];
if (window) {
[MBProgressHUD hideHUDForView:window animated:YES];
}
}
});
}
+ (void)showMsg:(NSString *)msg {
if (msg.length == 0) return;
dispatch_async(dispatch_get_main_queue(), ^{
UIWindow *window = [self _keyWindow];
if (!window) return;
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:window animated:YES];
hud.mode = MBProgressHUDModeText;
hud.bezelView.style = MBProgressHUDBackgroundStyleSolidColor;
hud.bezelView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.75];
hud.contentColor = [UIColor whiteColor];
hud.label.text = msg;
hud.label.font = [UIFont systemFontOfSize:14];
hud.removeFromSuperViewOnHide = YES;
// 2.0s 后自动消失
[hud hideAnimated:YES afterDelay:2.0];
});
}
#pragma mark - 字符串判断
BOOL stringIsEmpty (NSString *str)
{
if (str == nil || str == NULL)
{
return YES;
}
if ([str isKindOfClass:[NSNull class]])
{
return YES;
}
if ([str isKindOfClass:[NSString class]])
{
NSString * newStr = [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSSet *emptySet = [NSSet setWithObjects:@"", @"null", @"(null)", @"<null>", @"NULL", @"",@"kZero", nil];
if ([emptySet containsObject:str] || [emptySet containsObject:newStr]) {
return YES;
} else {
return [newStr length] == 0;
}
}
return NO;
}
BOOL stringIsNotEmpty (NSString *str)
{
return ! stringIsEmpty(str);
}
/// 字符串转数值
+(NSString *)stringValueFromStr:(NSString *)str {
if (stringIsEmpty(str)) {
return @"";
}
return str;
}
#pragma mark - 获取图片
+(UIImage *)imageWithName:(NSString *)name {
NSURL * url = [[NSBundle mainBundle] URLForResource:@"AMapNavIOSSDK" withExtension:@"bundle"];
NSBundle *containnerBundle = [NSBundle bundleWithURL:url];
NSString * path = [containnerBundle pathForResource:[NSString stringWithFormat:@"%@@2x.png" , name] ofType:nil];
UIImage * arrowImage = [[UIImage imageWithContentsOfFile:path] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
return arrowImage;
}
+(UIImage *)imageWithName3x:(NSString *)name {
NSURL * url = [[NSBundle mainBundle] URLForResource:@"AMapNavIOSSDK" withExtension:@"bundle"];
NSBundle *containnerBundle = [NSBundle bundleWithURL:url];
NSString * path = [containnerBundle pathForResource:[NSString stringWithFormat:@"%@@3x.png" , name] ofType:nil];
UIImage * arrowImage = [[UIImage imageWithContentsOfFile:path] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
return arrowImage;
}
@end
@@ -0,0 +1,19 @@
//
// AMapNavHttpUtil.h
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AMapNavHttpUtil : NSObject
+ (void)postRequestWithURL:(NSString *)urlString parameters:(id)parameters requestHeader:(NSDictionary *)headParam successHandler:(void (^)(NSDictionary *data, NSURLResponse *response))successHandler failureHandler:(void ( ^)(NSError *error))failureHandler;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,114 @@
//
// AMapNavHttpUtil.m
// AMapNavIOSSDK
//
// Created by admin on 2026/2/11.
//
#import "AMapNavHttpUtil.h"
#define AMapRequestMethod_POST @"POST"
@interface AMapNavHttpUtil ()
@property (nonatomic , copy) NSString * baseURL;
@end
@implementation AMapNavHttpUtil
+ (instancetype)sharedInstance {
static AMapNavHttpUtil *sharedInstance = nil;
static dispatch_once_t onceToken;
// dispatch_once确保下面的代码块只被执行一次
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
// 可以在这里进行一些初始化操作
});
return sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
// if (sdk.config.developmentModel) {
// _baseURL = kYTOTPAnalyticsSDKTestHost;
// }else {
// _baseURL = kYTOTPAnalyticsSDKProductionHost;
// }
}
return self;
}
+ (void)postRequestWithURL:(NSString *)urlString parameters:(id)parameters requestHeader:(NSDictionary *)headParam successHandler:(void (^)(NSDictionary *data, NSURLResponse *response))successHandler failureHandler:(void ( ^)(NSError *error))failureHandler {
[self requestWithMethod:AMapRequestMethod_POST URL:urlString parameters:parameters requestHeader:headParam successHandler:successHandler failureHandler:failureHandler];
}
// 使用NSURLSession发起网络请求
+ (void)requestWithMethod:(NSString *)method URL:(NSString *)urlString parameters:(id)parameters requestHeader:(NSDictionary *)headParam successHandler:(void (^)(NSDictionary *data, NSURLResponse *response))successHandler failureHandler:(void (^)(NSError *error))failureHandler {
if (!urlString) {
return;
}
// 创建URL
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@" , urlString]];
// 创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = method;
request.timeoutInterval = 30.0;
// 设置请求头,指定数据通讯格式为json
if (headParam) {
for (NSString *key in headParam.allKeys) {
if (headParam[key]) {
[request setValue:[NSString stringWithFormat:@"%@",headParam[key]] forHTTPHeaderField:key];
}
}
}
// 将参数转换为JSON数据
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:&error];
if (!jsonData) {}
if ([method isEqualToString: AMapRequestMethod_POST]) {
// 设置请求体
request.HTTPBody = jsonData;
}
__block NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"request error:%@" , error);
if (failureHandler) {
failureHandler(error);
}
} else {
if (successHandler) {
NSDictionary * dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if(dic){
NSLog(@"url: %@ , response data:%@", url , dic);
}
dispatch_async(dispatch_get_main_queue(), ^{
successHandler(dic, response);
});
// successHandler(dic, response);
}
}
[session finishTasksAndInvalidate];
session = nil;
}];
// 开始任务
[task resume];
}
@end

Some files were not shown because too many files have changed in this diff Show More