<file_sep>﻿using Core;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace App.Pages
{
    public class IndexModel : PageModel
    {
        [BindProperty]
        public string MapboxToken { get; set; }

        private readonly IOptions<AppSettings> _options;
        public IndexModel(IOptions<AppSettings> options)
        {
            this._options = options;
        }

        public void OnGet()
        {
            this.MapboxToken = this._options.Value.MapboxToken;
        }
    }
}
<file_sep>using Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Core.Data.Service.GoogleServices;

namespace App
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // configure strongly typed settings objects
            var section = Configuration.GetSection("AppSettings");
            // services.Configure<AppSettings>(section);
            var appSettings = section.Get<AppSettings>();

            // INFO: must have, let IOptions<T> knows and worked out
            services.Configure<AppSettings>(section);

            services.AddRazorPages();
            services.AddCors(c =>
            {
                c.AddPolicy("AllowOrigin", options => options
                    .AllowAnyOrigin()
                    .AllowAnyHeader()
                    .AllowAnyMethod()
                );
            });
            // ref: https://github.com/aspnet/AspNetCore/issues/9348
            services.AddRouting(options => options.SuppressCheckForUnhandledSecurityMetadata = true);

            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddHttpClient();
            services.AddTransient<Place, Place>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
            });
            app.UseCors(options => options.AllowAnyOrigin());

            AppSettings.WebRootPath = env.WebRootPath;
            AppSettings.ContentRootPath = env.ContentRootPath;
        }
    }
}
<file_sep>﻿using Core.Data.GoogleServices;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace Core.Data.Service.GoogleServices
{
    public class Place
    {
        private readonly IOptions<AppSettings> _options;
        private readonly IHttpClientFactory _httpClientFactory;

        public Place(IOptions<AppSettings> options, IHttpClientFactory httpClientFactory)
        {
            this._options = options;
            this._httpClientFactory = httpClientFactory;
        }
        public async Task<GooglePlaceData> Nearbysearch(double lng, double lat, string name, string lang)
        {
            return await Nearbysearch(lng, lat, name, 1000, "airport", lang);
        }

        public async Task<GooglePlaceData> Nearbysearch(double lng, double lat, string name, int radius, string type, string lang)
        {
            var api = string.Format(
                this._options.Value?.GooglePlaceApi.Nearbysearch,
                this._options.Value?.GoogleApiToken,
                lat,
                lng,
                radius,
                name,
                type,
                lang);
            var jsonStr = await this._httpClientFactory.CreateClient().GetStringAsync(api);
            var data = JsonSerializer.Deserialize<GooglePlaceData>(jsonStr);
            return data;
        }

        public async Task<GooglePlaceData2> NearbySearch(string name)
        {
            var api = string.Format(
                this._options.Value?.GooglePlaceApi.FindFromText,
                this._options.Value?.GoogleApiToken,
                name);
            var jsonStr = await this._httpClientFactory.CreateClient().GetStringAsync(api);
            try
            {
                var jn = jsonStr.Replace("\n", "");
                var temp = JsonSerializer.Deserialize<GooglePlaceData2>(jn);
                if (temp != null)
                {
                    return temp;
                }
                else
                {
                    return new GooglePlaceData2 { };
                }
            }
            catch (Exception)
            {
                throw;
            }
        }

        public async Task<GooglePlaceDetailData> Detail(string placeid, string lang)
        {
            var api = string.Format(this._options.Value?.GooglePlaceApi.Detail, this._options.Value?.GoogleApiToken, placeid, lang);
            var jsonStr = await this._httpClientFactory.CreateClient().GetStringAsync(api);
            var data = JsonSerializer.Deserialize<GooglePlaceDetailData>(jsonStr);
            return data;
        }

        public async Task<string> Photo(string photoreference, int maxWidth)
        {
            var api = string.Format(this._options.Value?.GooglePlaceApi.Photo, this._options.Value?.GoogleApiToken, photoreference, maxWidth);
            var strm = await this._httpClientFactory.CreateClient().GetStreamAsync(api);
            var base64Str = Convert.ToBase64String(StreamToBytes(strm));
            return base64Str;
        }
        private static byte[] StreamToBytes(System.IO.Stream stream)
        {
            using var memoryStream = new System.IO.MemoryStream();
            stream.CopyTo(memoryStream);
            return memoryStream.ToArray();
        }
    }
}
<file_sep>﻿using System.Reflection;

namespace Core
{
    public class AppSettings
    {
        public string MapboxToken { get; set; }
        public string GoogleApiToken { get; set; }
        public GooglePlaceApi GooglePlaceApi { get; set; }


        public static string WebRootPath { get; set; }
        public static string ContentRootPath { get; set; }

        public static string Version
        {
            get
            {
                return typeof(AppSettings)
                    .GetTypeInfo()
                    .Assembly
                    .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
                    .InformationalVersion;
            }
        }
        public static string OSDescription
        {
            get
            {
                return System.Runtime.InteropServices.RuntimeInformation.OSDescription;
            }
        }
    }
    
    public class GooglePlaceApi
    {
        public string Nearbysearch { get; set; }
        public string FindFromText { get; set; }
        public string Detail { get; set; }
        public string Photo { get; set; }
    }
}
<file_sep>﻿using System.Collections.Generic;

namespace Core.Data.GoogleServices
{
    public class GooglePlaceData2
    {
        public IEnumerable<TextPlaceData> candidates { get; set; }
        public string status { get; set; }
    }

    public class TextPlaceData
    {
        public string formatted_address { get; set; }
        public Geometry geometry { get; set; }
        public string name { get; set; }
        public OpeningHours opening_hours { get; set; }
        public IEnumerable<Photo> photos { get; set; }
        public double rating { get; set; }
    }
    public class OpeningHours
    {
        public bool open_now { get; set; }
    }



}
<file_sep>﻿using Core.Data.GoogleServices;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;

namespace Core.Data.Api
{
    [Route("api/g")]
    [ApiController]
    public class GoogleController : ControllerBase
    {
        private readonly Service.GoogleServices.Place _placeService;

        public GoogleController(Service.GoogleServices.Place placeService)
        {
            this._placeService = placeService;
        }

        [HttpGet("nearby/{lng}/{lat}/{name}/{lang?}")]
        public async Task<ActionResult<GooglePlaceData>> GetNearby(double lng, double lat, string name, string lang)
        {
            var ret = await this._placeService.Nearbysearch(lng, lat, name, lang);
            // return new JsonResult(ret); // same results by using Ok(ret);
            return Ok(ret);
        }

        [HttpGet("find/{name}")]
        public async Task<ActionResult<GooglePlaceData2>> FindFromText(string name)
        {
            var ret = await this._placeService.NearbySearch(name);
            return Ok(ret);
        }

        [HttpGet("detail/{placeid}/{lang?}")]
        public async Task<ActionResult<GooglePlaceDetailData>> GetDetail(string placeid, string lang)
        {
            var ret = await this._placeService.Detail(placeid, lang);
            return Ok(ret);
        }

        [HttpGet("r/{lng}/{lat}/{name}/{width}/{lang?}")]
        public async Task<IActionResult> GetMixedUp(double lng, double lat, string name, int width, string lang)
        {
            var nearby = await this._placeService.Nearbysearch(lng, lat, name, lang);
            if (nearby == null || nearby.status != "OK" || !nearby.results.Any())
            {
                return Ok(new
                {
                    name = string.Empty,
                    photo = string.Empty,
                    formattedAddress = string.Empty,
                    phoneNumber = string.Empty,
                    website = string.Empty
                });
            }

            if (!nearby.results.Any())
            {
                return Ok(new
                {
                    name = string.Empty,
                    photo = string.Empty,
                    formattedAddress = string.Empty,
                    phoneNumber = string.Empty,
                    website = string.Empty
                });
            }
            else
            {
                var result = nearby.results.FirstOrDefault();
                var detail = await this._placeService.Detail(result.place_id, lang);
                var detailResult = detail?.result;

                var photos = result.photos;
                var photo = string.Empty;
                if (photos != null && photos.Any())
                {
                    photo = await this._placeService.Photo(photos.FirstOrDefault().photo_reference, width);
                }

                return Ok(new
                {
                    detailResult.name,
                    photo,
                    formattedAddress = detailResult?.formatted_address,
                    phoneNumber = detailResult?.international_phone_number,
                    detailResult?.website
                });
            }
        }

        [HttpGet("r/{name}")]
        public async Task<IActionResult> GetMixedUp(string name)
        {
            var detail = await this._placeService.NearbySearch(name);
            if (detail != null && detail.status == "OK")
            {
                if (detail.candidates.Any())
                {
                    var candidate = detail.candidates.FirstOrDefault();
                    var photos = candidate.photos;
                    var photoBase64 = "";
                    if (photos.Any())
                    {
                        var photo = candidate.photos.FirstOrDefault();
                        photoBase64 = await this._placeService.Photo(photo.photo_reference, photo.width);
                    }
                    return Ok(new
                    {
                        candidate.name,
                        photo = photoBase64,
                        formattedAddress = candidate.formatted_address,
                        website = candidate.photos.Any() ? candidate.photos.FirstOrDefault().html_attributions.FirstOrDefault() : string.Empty
                    });
                }
                return Ok(new
                {
                    name = string.Empty,
                    photo = string.Empty,
                    formattedAddress = string.Empty,
                    website = string.Empty
                });
            }
            else
            {
                return Ok(new
                {
                    name = string.Empty,
                    photo = string.Empty,
                    formattedAddress = string.Empty,
                    website = string.Empty
                });
            }
        }
    }
}
<file_sep>﻿using System.Collections.Generic;

namespace Core.Data
{
    /// <summary>
    /// feature data use for mapbox
    /// </summary>
    public class FeatureCollection
    {
        public string Type { get; set; } = "FeatureCollection";
        public IEnumerable<FeaturePoint> Features { get; set; }
    }

    public class FeaturePoint
    {
        public string Type { get; set; } = "Feature";
        public Geometry Geometry { get; set; }
        public Property Properties { get; set; }
    }
    public class Geometry
    {
        public Geometry(double longitude, double latitude)
        {
            this.Coordinates = new[] { longitude, latitude };
        }
        public string Type { get; set; } = "Point";
        public double[] Coordinates { get; set; }
    }
    public class Property
    {
        public string Name { get; set; }
    }
}
<file_sep>﻿namespace Core.Data
{
    /// <summary>
    /// main data: scenery from excel file
    /// </summary>
    public class Scenery
    {
        public string Name { get; set; }
        public Coordinates Coordinates { get; set; }
    }

    public class Coordinates
    {
        public double Longitude { get; set; }
        public double Latidue { get; set; }
    }
}
<file_sep>﻿using Core.Data.Srvice;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Core.Data.Api
{
    [Route("api/[controller]")]
    [ApiController]
    public class SceneriesController : ControllerBase
    {
        [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Client)]
        [HttpGet]
        [EnableCors("AllowOrigin")]
        public async Task<ActionResult<IEnumerable<FeatureCollection>>> Get()
        // public async Task<IActionResult> Get()
        {
            var sceneries = await Task.FromResult(FileHandlerService.XlsHandler());
            var result = new FeatureCollection
            {
                Features = sceneries.Select(p => new FeaturePoint
                {
                    Geometry = new Geometry(p.Coordinates.Longitude, p.Coordinates.Latidue),
                    Properties = new Property
                    {
                        Name = p.Name
                    }
                })
            };
            return Ok(result);
        }
    }
}
<file_sep>﻿namespace Core.Data.GoogleServices
{
    public class GooglePlaceDetailData
    {
        public string[] html_attributions { get; set; }
        public Dresult result { get; set; }
        public string status { get; set; }
    }
    public class Dresult
    {
        // only grab info needed

        public string adr_address { get; set; }
        public string formatted_address { get; set; }
        public string formatted_phone_number { get; set; }
        public string international_phone_number { get; set; }
        public string name { get; set; }
        public string url { get; set; }
        public string vicinity { get; set; }
        public string website { get; set; }
    }
}
<file_sep>﻿using ExcelDataReader;
using System;
using System.Collections.Generic;
using System.IO;

namespace Core.Data.Srvice
{
    public class FileHandlerService
    {
        public static IEnumerable<Scenery> XlsHandler()
        {
            System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);

            try
            {
                // remember, it's bad for unit test, bad smells: too much dependency there
                var data = new List<Scenery>();
                var xls = Path.Combine(AppSettings.ContentRootPath, "sceneries.xlsx");
                using var stream = File.Open(xls, FileMode.Open, FileAccess.Read);
                using var reader = ExcelReaderFactory.CreateReader(stream);
                do
                {
                    while (reader.Read())
                    {
                        // in the excel file, 0 is name column, 4 is coordinates column
                        if (reader.GetString(0) != null && reader.GetString(4) != null)
                        {
                            var name = Convert.ToString(reader.GetString(0));
                            var coordinatesString = Convert.ToString(reader.GetString(4));
                            var spliter = coordinatesString.Split(",");
                            if (spliter.Length == 2)
                            {
                                double.TryParse(spliter[0], out var y); // latitude
                                double.TryParse(spliter[1], out var x); // longitude

                                if (x != 0 && y != 0)
                                {
                                    data.Add(new Scenery
                                    {
                                        Name = name,
                                        Coordinates = new Coordinates
                                        {
                                            Longitude = x,
                                            Latidue = y
                                        }
                                    });
                                }
                            }
                        }
                    }
                } while (reader.NextResult());

                return data;
            }
            catch(Exception)
            {
                throw;
            }
        }
    }
}
<file_sep>﻿let lang = '';

function rendermap(loc) {
    if (!loc || loc === null) { return; }

    mapboxgl.accessToken = document.getElementById('mapboxtoken').value;

    const map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/streets-v11',
        center: [ loc.coords.longitude, loc.coords.latitude ],
        zoom: 7 // starting zoom
    });

    const nav = new mapboxgl.NavigationControl();
    map.addControl(nav, 'bottom-right');

    map.on('load', () => {
        mapcallback(map);
    });
}

function mapcallback(map) {
    map.addSource('sceneries', {
        type: 'geojson',
        data: `${apiRoot}/sceneries`,
        cluster: true, // Enable clustering
        clusterRadius: 50, // Radius of each cluster when clustering points
        clusterMaxZoom: 6 // Max zoom to cluster points on
    });

    map.addLayer({
        id: 'sceneries',
        type: 'circle',
        source: 'sceneries',
        filter: ['!has', 'point_count'],
        paint: {
            'circle-color': '#a890f0',
            'circle-radius': 5,
            'circle-stroke-width': 1,
            'circle-stroke-color': '#fff'
        }
    });

    map.on('mouseenter', 'sceneries', (data) => {
        map.getCanvas().style.cursor = 'pointer';

        const name = data.features[0].properties.name;

        $('#card').modal('toggle');

        const api = `/api/g/r/${name}`;
        fetch(api)
            .then(resp => {
                if (resp.status === 200) {
                    return resp.json();
                }
                return {};
            })
            .then(data => {
                setSceneryProperties(data);
            })
            .catch(error => {
                console.log(error);
                document.getElementById('scenery-image').src = 'https://via.placeholder.com/400x200?text=Error+while+loading+data';
            });
    });

    map.on('mouseleave', 'sceneries', () => {
        map.getCanvas().style.cursor = '';
        closeInfoCard();
    });

    var placesAutocomplete = places({
        container: document.querySelector('#where-to-go'),
        type: 'city'
    });

    placesAutocomplete.on('change', e => {
        map.flyTo({
            center: [e.suggestion.latlng.lng, e.suggestion.latlng.lat],
            zoom: 9
        });
    });
}

function setSceneryProperties(data) {
    if (!data || data === null) { return; }

    if (data.photo && data.photo !== null) {
        document.getElementById('scenery-image').src = `data:image/png;base64,${data.photo}`;
    }
    else {
        document.getElementById('scenery-image').src = 'https://via.placeholder.com/500x300?text=No+Image+Found';
    }

    document.getElementById('name').innerText = data.name || '';
    document.getElementById('address').innerText = data.formattedAddress || '';
    document.getElementById('website').innerHTML = data.website || '';
}

function closeInfoCard() {
    document.getElementById('scenery-image').src = 'https://via.placeholder.com/500x300?text=loading';
    document.getElementById('name').innerText = '';
    document.getElementById('address').innerText = '';
    document.getElementById('website').innerHTML = '';

    $('#card').modal('toggle');
}

function getBaseUri(isApi = false) {
    const loc = window.location;
    const protocol = loc.protocol;
    const host = loc.host;
    if (isApi) {
        return `${protocol}//${host}/api`;
    }
    return `${protocol}//${host}`;
}

const apiRoot = getBaseUri(true);

(() => {
    if (navigator.geolocation) {
        // we get geolocation from browsers, to make map centered upon current location.
        navigator.geolocation.getCurrentPosition(rendermap);
    }
    // what if, geolocation did NOT support by browsers, or premission denied by users !?
})();
