Tính khoảng cách đường bay Airport Distance Map

https://codepen.io/shshaw/pen/vJNMQY

<div id="app" class="mileage">

  <svg class="mileage-map" ref="map" width="1200" height="780" viewBox="-120 -180 1200 780">

    <g class="mileage-map__states" ref="states"></g>

    <g ref="airports">
      <g class="airport" :class="{ 'airport--current' : airport.current }"
         v-for="airport in airports">
        <circle r="3" :cx="airport.x" :cy="airport.y" class="airport__marker" />
        <circle r="16" :cx="airport.x" :cy="airport.y" class="airport__range"
                @mousemove="airportSnap($event, airport)"
                @mouseup="airportClick($event, airport)"
                @mouseleave.self="airportLeave($event, airport)" />
      </g>
    </g>

    <path class="marker-connector" :d="markerConnect()"></path>

    <g class="airplane" ref="airplane">
      <path class="airplane__icon" d="M21 15.984l-8.016-2.484v5.484l2.016 1.5v1.5l-3.516-0.984-3.469 0.984v-1.5l1.969-1.5v-5.484l-7.969 2.484v-1.969l7.969-5.016v-5.484c0-0.844 0.656-1.5 1.5-1.5s1.5 0.656 1.5 1.5v5.484l8.016 5.016v1.969z" transform="translate(-8,-15) scale(1.2)"></path>
    </g>

    <g v-for="marker in markers" @mousedown="markerSet($event,marker)" class="marker" :class="{ 'marker--current' : marker.current }" :transform="'translate('+marker.x+','+marker.y+')'">
      <path fill="{{marker.fill}}" d="M0 0l28.592-28.592c15.78-15.78 15.908-41.24.128-57.02a40.424 40.424 0 0 0-57.124 57.2z"></path>
      <transition name="marker-fade">
        <text v-if="marker.airport" x="0" y="-42" text-anchor="middle" v-text="marker.airport.LocationID"></text>
      </transition>
    </g>
  </svg>

  <h1 class="mileage__calculations" v-show="distance">
    <span>{{ distance | numberWithCommas }}</span><small>mi</small>
  </h1>
  
  <div class="mileage__instructions">Drag the markers around to calculate distance</div>

</div>
// original
@text: #FFF;
@light: #16a8d3;
@medium: #006196;
@dark: #000;

// // blue
// @light: #30E3CA; // #16a8d3
// @medium: #11999E; // #006196
// @dark: #40514E; // #000;

// // blue-green
// //@text: #A4E5D9;
// @light: #C8F4DE;
// @medium: #66C6BA;
// @dark: #649DAD;

// // green
// @text: #196B69;
// @light: #DDFEE4;
// @medium: #28CC9E;
// @dark: #132F2B;

// // another blue
// @text: #ECF0F1;
// @light: #2980B9;
// @medium: #33CCCC;
// @dark: #2C3E50;

// // purple
// @text: #ECF0F1;
// @light: #BB99CD;
// @medium: #643579;
// @dark: #3D1860;

// // blue-gray
// @dark: #263849;
// @medium: #41506B;
// @medium: #35BCBF;
// @light: #90F6D7;

@dark: saturate(lighten(#F59D2A,5%),20%);
@medium: #EE7738;
//@dark: #2C3D4F;
@light: #34495D;

@import url('https://fonts.googleapis.com/css?family=Oswald');

html { height: 100%; display: flex; }
body { margin: auto; }

html { 
  background: @light; 
  text-align: center; 
  color: @text; 
  font-family: 'Oswald', sans-serif;
  font-weight: 300;
  font-smooth: auto;
  -webkit-font-smoothing: antialiased;
}

@spacing-base: 2rem;

.mileage__calculations { 
  margin: 0;
  font-size: 3rem;
  font-weight: inherit;
  small { font-size: 0.6em; color: @dark; } 
}

.mileage-map {
  display: block;
  width: auto;
  height: auto;
  max-width: 90%;
  max-height: 90vh;
  margin: auto auto 2rem;
  overflow: visible;
}

.mileage-map__states {
  stroke: @light;
  stroke-width: 2;
  fill: @medium;
}

.marker {
  fill: @dark;
  stroke: @text;
  stroke-width: 5;
  cursor: pointer;
  transition: opacity 0.3s linear;
  user-select: none;
  > * { transition: transform 0.3s cubic-bezier(.6,.0,.5,1); }

  text {
    stroke: none;
    fill: @text;
    font-family: inherit;
    font-size: 28px;
    font-weight: 200;
  }
}

.marker-fade {
  &-leave-active,
  &-enter-active { 
    transition: opacity .3s linear;
  }

  &-enter, 
  &-leave-to {
    opacity: 0;
  }
}

.marker--current {
  opacity: 0.8;
  pointer-events: none;

  > * { transform: scale(0.8); }
}

/* ////////////////////////////////////////////////////////////////////////// */

.airport { fill: #FFF; stroke-width: 2; stroke: transparent; transition: stroke 0.3s linear; }
.airport:hover { stroke: rgba(255,255,255,0.3); }
.airport--current,
.airport--current:hover { stroke: #FFF; stroke: rgba(255,255,255,0.8); }

.airport__range {
  fill: transparent;
  cursor: pointer;
}



////////////////////////////////////////


.marker-connector {
  fill: none;
  stroke: #FFF;
  stroke-width: 3;
  stroke-dasharray: 6 6;
  pointer-events: none;

  animation: marching-ants 1s linear 0s infinite;
}

@keyframes marching-ants {
  from { stroke-dashoffset: 0; }
  to { stroke-dashoffset: 12; }
}


.airplane {
  pointer-events: none;
  fill: #FFF;
  //stroke: @medium;
  //stroke-width: 0.5;
  opacity: 0;
}
console.clear();

new Vue({

  el: '#app',

  data: () => ({
    distance: null,
    airports: [],
    currentAirport: null,
    lastAirport: null,
    markers: [
      {
        airport: null,
        x: 200,
        y: 300,
        startX: 0,
        startY: 0,
        fill: '#f47825',
        current: false
      },
      {
        airport: null,
        x: 500,
        y: 100,
        startX: 0,
        startY: 0,
        fill: '#00b26b',
        current: false
      }
    ]
  }),

  filters: {
    numberWithCommas(val) {
      return ( val && val.toString ? val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : val );
    }
  },

  mounted() {

    // D3 Projection
    var projection = d3.geoAlbersUsa().scale([1500]); // scale things down so see entire US
    this.projection = projection;

    // D3 US States map
    fetch('https://s3-us-west-2.amazonaws.com/s.cdpn.io/39255/us-states.json')
      .then( response => response.json() )
      .then( (states) => {

      // Define path generator
      var path = d3.geoPath() // path generator that will convert GeoJSON to SVG paths
      .projection(projection); // tell path generator to use albersUsa projection

      // Bind the data to the SVG and create one path per GeoJSON feature
      d3.select( this.$refs.states )
        .selectAll("path")
        .data(states.features)
        .enter()
        .append("path")
        .attr("d", path);
    });

    // Airports
    fetch('https://s3-us-west-2.amazonaws.com/s.cdpn.io/39255/airports.json')
      .then( response => response.json() )
      .then( (airports) => {

      // Let's only focus on the top 100
      airports = airports.slice(0,100);
      var i = airports.length, d, proj;

      while(i--){
        d = airports[i];
        proj = projection([d.Lng, d.Lat]);

        if ( proj ) {
          d.x = proj[0];
          d.y = proj[1];
        } else {
          airports.splice(i, 1);
        }
      }

      this.airports = airports.reverse();

      var la = null;

      this.markers.forEach((marker)=>{
        var ra = this.randomAirport();
        if ( ra == la ) { ra = this.randomAirport(); }
        la = ra;
        marker.airport = ra;
        marker.x = ra.x;
        marker.y = ra.y;
      });
      this.markerDistance();
    });


  },

  methods: {

    randomAirport(){
      return this.airports[ Math.floor(Math.random() * this.airports.length) ];
    },

    markerSet(e, marker){
      if ( e ) { e.preventDefault(); }

      marker = marker || this.markers[0];
      marker.airport = null;
      marker.current = true;
      marker.startX = marker.x;
      marker.startY = marker.y;

      this.currentAirport = null;
      this.currentMarker = marker;

      if ( this.airplaneTween ) {

        var oldTween = this.airplaneTween;

        var tl = new TimelineLite();
        tl.to(this.$refs.airplane,0.2,{
          opacity: 0,
          ease: "Linear.easeNone",
          onComplete: function(){
            if ( oldTween ) { oldTween.kill(); }
          }
        });

        this.airplaneFade = tl;
      }

      this.markerDrag(e);
      document.addEventListener('mousemove',this.markerDrag);
      document.addEventListener('mouseup', this.markerStop);
      this.$refs.map.addEventListener('mouseleave', this.markerLeave);
    },

    markerDrag(e){
      this.currentMarker.airport = this.currentAirport;

      if ( this.currentAirport ) {
        this.currentMarker.x = this.currentAirport.x;
        this.currentMarker.y = this.currentAirport.y;
      } else {
        d3.event = e;
        var mouse = d3.mouse(this.$refs.map);
        this.currentMarker.x = mouse[0];
        this.currentMarker.y = mouse[1];
      }
      this.markerDistance();
    },

    markerLeave(){
      this.currentMarker.x = this.currentMarker.startX;
      this.currentMarker.y = this.currentMarker.startY;

      this.markerStop();
    },

    markerStop(){
      console.log('stop!');
      document.removeEventListener('mousemove',this.markerDrag);
      document.removeEventListener('mouseup', this.markerStop);
      this.$refs.map.removeEventListener('mouseleave', this.markerLeave);

      this.currentMarker.current = false;
      this.currentMarker = null;

      this.markerDistance();
    },

    markerConnect(){
      var m1 = this.markers[1],
          m2 = this.markers[0];

      if ( m1.x < m2.x ) {
        m1 = this.markers[0];
        m2 = this.markers[1];
      }

      var dx = m2.x - m1.x,
          dy = m2.y - m1.y,
          dr = Math.sqrt(dx * dx + dy * dy);

      return "M" + m2.x + "," + m2.y + "A" + dr + "," + dr + " 0 0,1 " + m1.x + "," + m1.y ;
    },

    calcDistance(lat1, lon1, lat2, lon2, unit) {
      var radlat1 = Math.PI * lat1/180
      var radlat2 = Math.PI * lat2/180
      var theta = lon1-lon2
      var radtheta = Math.PI * theta/180
      var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
      dist = Math.acos(dist)
      dist = dist * 180/Math.PI
      dist = dist * 60 * 1.1515
      if (unit=="K") { dist = dist * 1.609344 }
      if (unit=="N") { dist = dist * 0.8684 }
      return dist;

    },

    markerDistance(){
      var marker1 = this.markers[0];
      var latLng1 = this.projection.invert([ marker1.x, marker1.y ]).reverse();

      var marker2 = this.markers[1];
      var latLng2 = this.projection.invert([ marker2.x, marker2.y ]).reverse();

      this.distance = Math.round( this.calcDistance(latLng1[0], latLng1[1], latLng2[0], latLng2[1]) );
      this.airplaneAnimate();
    },

    airportSnap(e, airport){
      if ( airport !== this.currentAirport ) {
        airport.current = true;
        this.currentAirport = airport;
        if ( this.currentMarker ) { this.currentMarker.airport = airport; }
      }
    },

    airportClick(e,airport){
      if ( !this.currentMarker ) { this.markerSet(e); }
      this.currentAirport = airport;
      this.currentMarker.x = this.currentAirport.x;
      this.currentMarker.y = this.currentAirport.y;
      if ( this.currentMarker ) { this.currentMarker.airport = airport; }
    },

    airportLeave(e,airport){
      airport.current = false;
      this.currentAirport = null;
      this.lastAirport = airport;
    },

    airplaneAnimate(){
      var path = this.markerConnect();
      var newTween = new TimelineMax({ repeat: -1, delay: -0.2 });
      var duration = Math.min( this.distance / 80, 15 );
      var opacityDuration = Math.min(duration * 0.2, 0.3);

      if ( this.airplaneFade && this.airplaneFade.isActive() ) {
        newTween.pause();

        this.airplaneFade.eventCallback('onComplete',function(){
          newTween.play();
        });
      } else if ( this.airplaneTween ) {
        this.airplaneTween.kill();
      }

      var bez = MorphSVGPlugin.pathDataToBezier(path)

      newTween.to(this.$refs.airplane, duration, {
        bezier: {
          values: bez,
          curviness: 1,
          autoRotate: -90,
          type:"cubic"
        },
        reversed: true,
        ease: Linear.easeNone
      }, 0);

      newTween.fromTo(this.$refs.airplane, opacityDuration, {
        opacity: 0
      },{
        opacity: 1,
        delay: opacityDuration/2,
        ease: "Linear.easeNone"
      }, 0);

      newTween.to(this.$refs.airplane, opacityDuration, {
        opacity: 0,
        ease: 'Linear.easeNone'
      }, '-='+opacityDuration);

      this.airplaneTween = newTween;
    },

  }
});

Last updated