[fix]GetNext
大石头 编写于 2024-06-25 16:49:36
X
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using NewLife.Data;
using NewLife.Serialization;

namespace NewLife.Yun
{
    /// <summary>高德地图</summary>
    /// <remarks>
    /// 参考地址 http://lbs.amap.com/api/webservice/guide/api/georegeo/#geo
    /// </remarks>
    [DisplayName("高德地图")]
    public class AMap : Map, IMap
    {
        #region 构造
        /// <summary>高德地图</summary>
        public AMap()
        {
            AppKey = "" +
                // 大石头
                "99ac084eb7dd8015fe0ff4404fa800da," +
                "37262598ce2e94f31349ce892b4dbde1," +
                "c313359ec97eb28b57861c2ba177daef," +
                "bcd72261d7c2e9a00cea3ba5d234eda6," +
                "33cad9379e0592a40185c2e4faf10348," +
                "18a6fa58d5ed2ff21711671c2f07cd3b," +
                "cba659ee8bff537bd78e1af625e29c7e," +
                "8a02a1813747a77cc202896ace9d50cc," +
                "953d44e2b8f1a7f126b5970dd2883b2f," +
                "3607f421048ff109ba56f36c0e77d3a1," +
                "" +
                // 六条
                "2aada76e462af71e1b67ba1df22d0fa4," +
                "038a84bf20e8306fdd2203110739110c," +
                "29360e6eeb7b921d644cde3068ddf24f," +
                "a8e5e3e7b4068be9c525bd2b7854eb20," +
                "9935cf01abd570532ab7a19f83f905d3," +
                "ecacc934a6529b39513ea2bfa8a03def," +
                "08c70a500587c1006e10e4a096cb6b58," +
                "3508dadf3777531cef63bdc061ac020f," +
                "331566353c89521faffd84af22cd4f5f," +
                "6f19a71c6fd71baf54680eb63c4d5fce," +
                "" +
                // 照月
                "e21e2089c19945c83d3ed8cecbcbb685," +
                "3ca359e489f0251de0255ec1f53b0c70," +
                "1c3fa2a36a844b90ec16cb160a46478b," +
                "2dbec473a85f148b6392289385fee01e," +
                "4e09815630bcc72af47e9b3123c3a1f4," +
                "43c6c1d607b62118951e14830d171cbd," +
                "10858d92ddcabd5ddf5e41ab142e5ca0," +
                "d14b543d43be7d4a00ffc19a93487a44," +
                "9cf4e3e6e6bf207943f47d517818cb32," +
                "a5ebae758cf05562f2db8b7ff296876b," +
                "" +
                // 老邱
                "88518231bdda6c6eec394488b9c456fd," +
                "d8c5125ae7947e1e7eaac6c92a898801," +
                "001d285749a8cef63b5d1aaed8333e66," +
                "cd671f341ef0f25b169d4ea780514e63," +
                "ab086ac2309ae555a64baa2b32beefd0," +
                "2bd19587c3f9eefefe77ecfccec05b7a," +
                "c751fd89fbde2572956e09e3a09dee41," +
                "0d751f074063ff308b93cae343cad9f2," +
                "8e3802ad274b4c079619e772671f851f";
            KeyName = "key";
            //CoordType = "wgs84ll";
        }
        #endregion

        #region 方法
        /// <summary>远程调用</summary>
        /// <param name="url">目标Url</param>
        /// <param name="result">结果字段</param>
        /// <returns></returns>
        protected override async Task<T> InvokeAsync<T>(String url, String result)
        {
            var dic = await base.InvokeAsync<IDictionary<String, Object>>(url, result);
            if (dic == null || dic.Count == 0) return default;

            var status = dic["status"].ToInt();
            if (status != 1)
            {
                var msg = dic["info"] + "";

                // 删除无效密钥
                if (IsValidKey(msg)) RemoveKey(LastKey);

                return !ThrowException ? default(T) : throw new Exception(msg);
            }

            if (result.IsNullOrEmpty()) return (T)dic;

            return (T)dic[result];
        }
        #endregion

        #region 地址编码
        /// <summary>查询地址的经纬度坐标</summary>
        /// <param name="address"></param>
        /// <param name="city"></param>
        /// <returns></returns>
        public async Task<IDictionary<String, Object>> GetGeocoderAsync(String address, String city = null)
        {
            if (address.IsNullOrEmpty()) throw new ArgumentNullException(nameof(address));

            // 编码
            address = HttpUtility.UrlEncode(address);
            city = HttpUtility.UrlEncode(city);

            var url = $"http://restapi.amap.com/v3/geocode/geo?address={address}&city={city}&output=json";

            var list = await InvokeAsync<IList<Object>>(url, "geocodes");
            return list?.FirstOrDefault() as IDictionary<String, Object>;
        }

        /// <summary>查询地址获取坐标</summary>
        /// <param name="address">地址</param>
        /// <param name="city">城市</param>
        /// <param name="formatAddress">是否格式化地址。高德地图默认已经格式化地址</param>
        /// <returns></returns>
        public async Task<GeoAddress> GetGeoAsync(String address, String city = null, Boolean formatAddress = false)
        {
            var rs = await GetGeocoderAsync(address, city);
            if (rs == null || rs.Count == 0) return null;

            var gp = new GeoPoint();

            var ds = (rs["location"] + "").Split(',');
            if (ds != null && ds.Length >= 2)
            {
                gp.Longitude = ds[0].ToDouble();
                gp.Latitude = ds[1].ToDouble();
            }

            var geo = new GeoAddress();
            var reader = new JsonReader();
            reader.ToObject(rs, null, geo);

            geo.Code = rs["adcode"].ToInt();

            if (rs["township"] is IList<Object> ts && ts.Count > 0) geo.Township = ts[0] + "";
            if (rs["number"] is IList<Object> ns && ns.Count > 0) geo.StreetNumber = ns[0] + "";

            geo.Location = gp;

            if (formatAddress)
            {
                var geo2 = await GetGeoAsync(gp);
                if (geo2 != null)
                {
                    geo = geo2;
                    if (geo.Level.IsNullOrEmpty()) geo.Level = rs["level"] + "";
                }
            }

            {
                var addr = rs["formatted_address"] + "";
                if (!addr.IsNullOrEmpty()) geo.Address = addr;
            }
            // 替换竖线
            if (!geo.Address.IsNullOrEmpty()) geo.Address = geo.Address.Replace("|", null);

            return geo;
        }
        #endregion

        #region 逆地址编码
        /// <summary>根据坐标获取地址</summary>
        /// <remarks>
        /// http://lbs.amap.com/api/webservice/guide/api/georegeo/#regeo
        /// </remarks>
        /// <param name="point"></param>
        /// <returns></returns>
        public async Task<IDictionary<String, Object>> GetGeocoderAsync(GeoPoint point)
        {
            if (point.Longitude < 0.1 || point.Latitude < 0.1) throw new ArgumentNullException(nameof(point));

            var url = $"http://restapi.amap.com/v3/geocode/regeo?location={point.Longitude},{point.Latitude}&extensions=base&output=json";

            return await InvokeAsync<IDictionary<String, Object>>(url, "regeocode");
        }

        /// <summary>根据坐标获取地址</summary>
        /// <param name="point"></param>
        /// <returns></returns>
        public async Task<GeoAddress> GetGeoAsync(GeoPoint point)
        {
            var rs = await GetGeocoderAsync(point);
            if (rs == null || rs.Count == 0) return null;

            var geo = new GeoAddress
            {
                Address = rs["formatted_address"] + ""
            };
            geo.Location = new GeoPoint
            {
                Longitude = point.Longitude,
                Latitude = point.Latitude
            };
            if (rs["addressComponent"] is IDictionary<String, Object> component)
            {
                var reader = new JsonReader();
                reader.ToObject(component, null, geo);

                geo.Code = component["adcode"].ToInt();

                geo.Township = null;
                geo.Towncode = null;
                if (component["township"] is String ts) geo.Township = ts;
                if (component["towncode"] is String tc) geo.Towncode = tc;

                // 去掉乡镇代码后面多余的0
                var tcode = geo.Towncode;
                if (!tcode.IsNullOrEmpty() && tcode.Length > 6 + 3) geo.Towncode = tcode.TrimEnd("000");

                if (component["streetNumber"] is IDictionary<String, Object> sn && sn.Count > 0)
                {
                    geo.Street = sn["street"] + "";
                    geo.StreetNumber = sn["number"] + "";
                }
            }

            geo.Location = point;
            // 替换竖线
            if (!geo.Address.IsNullOrEmpty()) geo.Address = geo.Address.Replace("|", null);

            return geo;
        }
        #endregion

        #region 路径规划
        /// <summary>计算距离和驾车时间</summary>
        /// <remarks>
        /// http://lbs.amap.com/api/webservice/guide/api/direction
        /// 
        /// type:
        /// 0:直线距离
        /// 1:驾车导航距离(仅支持国内坐标)。
        /// 必须指出,当为1时会考虑路况,故在不同时间请求返回结果可能不同。
        /// 此策略和driving接口的 strategy = 4策略一致
        /// 2:公交规划距离(仅支持同城坐标)
        /// 3:步行规划距离(仅支持5km之间的距离)
        /// 
        /// distance    路径距离,单位:米
        /// duration    预计行驶时间,单位:秒
        /// </remarks>
        /// <param name="origin"></param>
        /// <param name="destination"></param>
        /// <param name="type">路径计算的方式和方法</param>
        /// <returns></returns>
        public async Task<Driving> GetDistanceAsync(GeoPoint origin, GeoPoint destination, Int32 type = 1)
        {
            if (origin == null || origin.Longitude < 1 && origin.Latitude < 1) throw new ArgumentNullException(nameof(origin));
            if (destination == null || destination.Longitude < 1 && destination.Latitude < 1) throw new ArgumentNullException(nameof(destination));

            if (type <= 0) type = 1;
            var url = $"http://restapi.amap.com/v3/distance?origins={origin.Longitude},{origin.Latitude}&destination={destination.Longitude},{destination.Latitude}&type={type}&output=json";

            var list = await InvokeAsync<IList<Object>>(url, "results");
            if (list == null || list.Count == 0) return null;

            if (list.FirstOrDefault() is not IDictionary<String, Object> geo) return null;

            var rs = new Driving
            {
                Distance = geo["distance"].ToInt(),
                Duration = geo["duration"].ToInt()
            };

            return rs;
        }
        #endregion

        #region 行政区划
        /// <summary>行政区划</summary>
        /// <remarks>
        /// http://lbs.amap.com/api/webservice/guide/api/district
        /// </remarks>
        /// <param name="keywords">查询关键字</param>
        /// <param name="subdistrict">设置显示下级行政区级数</param>
        /// <param name="code">按照指定行政区划进行过滤,填入后则只返回该省/直辖市信息</param>
        /// <returns></returns>
        public async Task<IList<GeoArea>> GetAreaAsync(String keywords, Int32 subdistrict = 1, Int32 code = 0)
        {
            if (keywords.IsNullOrEmpty()) throw new ArgumentNullException(nameof(keywords));

            // 编码
            keywords = HttpUtility.UrlEncode(keywords);

            var url = $"http://restapi.amap.com/v3/config/district?keywords={keywords}&subdistrict={subdistrict}&filter={code}&extensions=base&output=json";

            var list = await InvokeAsync<IList<Object>>(url, "districts");
            if (list == null || list.Count == 0) return null;

            if (list.FirstOrDefault() is not IDictionary<String, Object> geo) return null;

            var addrs = GetArea(geo, 0);

            return addrs;
        }

        private IList<GeoArea> GetArea(IDictionary<String, Object> geo, Int32 parentCode)
        {
            if (geo == null || geo.Count == 0) return null;

            var addrs = new List<GeoArea>();

            var root = new GeoArea();
            new JsonReader().ToObject(geo, null, root);
            root.Code = geo["adcode"].ToInt();
            if (parentCode > 0) root.ParentCode = parentCode;

            addrs.Add(root);

            if (geo["districts"] is IList<Object> childs && childs.Count > 0)
            {
                foreach (var item in childs)
                {
                    if (item is IDictionary<String, Object> geo2)
                    {
                        var rs = GetArea(geo2, root.Code);
                        if (rs != null && rs.Count > 0) addrs.AddRange(rs);
                    }
                }
            }

            return addrs;
        }
        #endregion

        #region 密钥管理
        private readonly String[] _KeyWords = new[] { "TOO_FREQUENT", "LIMIT", "NOMATCH", "RECYCLED" };
        /// <summary>是否无效Key。可能禁用或超出限制</summary>
        /// <param name="result"></param>
        /// <returns></returns>
        protected override Boolean IsValidKey(String result)
        {
            if (result.IsNullOrEmpty()) return false;

            if (_KeyWords.Any(e => result.Contains(e))) return true;

            return base.IsValidKey(result);
        }
        #endregion
    }
}