text: // @flow
  
  import Point from '@mapbox/point-geometry';
  
  import StyleLayer from '../style_layer';
  import LineBucket from '../../data/bucket/line_bucket';
  import {RGBAImage} from '../../util/image';
  import {polygonIntersectsBufferedMultiLine} from '../../util/intersection_tests';
  import {getMaximumPaintValue, translateDistance, translate} from '../query_utils';
  import properties from './line_style_layer_properties';
  import {extend} from '../../util/util';
  import EvaluationParameters from '../evaluation_parameters';
  import renderColorRamp from '../../util/color_ramp';
  import {Transitionable, Transitioning, Layout, PossiblyEvaluated, DataDrivenProperty} from '../properties';
  
  import type {FeatureState} from '../../style-spec/expression';
  import type {Bucket, BucketParameters} from '../../data/bucket';
  import type {LayoutProps, PaintProps} from './line_style_layer_properties';
  import type Transform from '../../geo/transform';
  import type Texture from '../../render/texture';
  import type {LayerSpecification} from '../../style-spec/types';
  
  class LineFloorwidthProperty extends DataDrivenProperty<number> {
      useIntegerZoom: true;
  
      possiblyEvaluate(value, parameters) {
          parameters = new EvaluationParameters(Math.floor(parameters.zoom), {
              now: parameters.now,
              fadeDuration: parameters.fadeDuration,
              zoomHistory: parameters.zoomHistory,
              transition: parameters.transition
          });
          return super.possiblyEvaluate(value, parameters);
      }
  
      evaluate(value, globals, feature, featureState) {
          globals = extend({}, globals, {zoom: Math.floor(globals.zoom)});
          return super.evaluate(value, globals, feature, featureState);
      }
  }
  
  const lineFloorwidthProperty = new LineFloorwidthProperty(properties.paint.properties['line-width'].specification);
  lineFloorwidthProperty.useIntegerZoom = true;
  
  class LineStyleLayer extends StyleLayer {
      _unevaluatedLayout: Layout<LayoutProps>;
      layout: PossiblyEvaluated<LayoutProps>;
  
      gradient: ?RGBAImage;
      gradientTexture: ?Texture;
  
      _transitionablePaint: Transitionable<PaintProps>;
      _transitioningPaint: Transitioning<PaintProps>;
      paint: PossiblyEvaluated<PaintProps>;
  
      constructor(layer: LayerSpecification) {
          super(layer, properties);
      }
  
      _handleSpecialPaintPropertyUpdate(name: string) {
          if (name === 'line-gradient') {
              this._updateGradient();
          }
      }
  
      _updateGradient() {
          const expression = this._transitionablePaint._values['line-gradient'].value.expression;
          this.gradient = renderColorRamp(expression, 'lineProgress');
          this.gradientTexture = null;
      }
  
      recalculate(parameters: EvaluationParameters, availableImages: Array<string>) {
          super.recalculate(parameters, availableImages);
  
          (this.paint._values: any)['line-floorwidth'] =
              lineFloorwidthProperty.possiblyEvaluate(this._transitioningPaint._values['line-width'].value, parameters);
      }
  
      createBucket(parameters: BucketParameters<*>) {
          return new LineBucket(parameters);
      }
  
      queryRadius(bucket: Bucket): number {
          const lineBucket: LineBucket = (bucket: any);
          const width = getLineWidth(
              getMaximumPaintValue('line-width', this, lineBucket),
              getMaximumPaintValue('line-gap-width', this, lineBucket));
          const offset = getMaximumPaintValue('line-offset', this, lineBucket);
          return width / 2 + Math.abs(offset) + translateDistance(this.paint.get('line-translate'));
      }
  
      queryIntersectsFeature(queryGeometry: Array<Point>,
                             feature: VectorTileFeature,
                             featureState: FeatureState,
                             geometry: Array<Array<Point>>,
                             zoom: number,
                             transform: Transform,
                             pixelsToTileUnits: number): boolean {
          const translatedPolygon = translate(queryGeometry,
              this.paint.get('line-translate'),
              this.paint.get('line-translate-anchor'),
              transform.angle, pixelsToTileUnits);
          const halfWidth = pixelsToTileUnits / 2 * getLineWidth(
              this.paint.get('line-width').evaluate(feature, featureState),
              this.paint.get('line-gap-width').evaluate(feature, featureState));
          const lineOffset = this.paint.get('line-offset').evaluate(feature, featureState);
          if (lineOffset) {
              geometry = offsetLine(geometry, lineOffset * pixelsToTileUnits);
          }
  
          return polygonIntersectsBufferedMultiLine(translatedPolygon, geometry, halfWidth);
      }
  
      isTileClipped() {
          return true;
      }
  }
  
  export default LineStyleLayer;
  
  function getLineWidth(lineWidth, lineGapWidth) {
      if (lineGapWidth > 0) {
          return lineGapWidth + 2 * lineWidth;
      } else {
          return lineWidth;
      }
  }
  
  function offsetLine(rings, offset) {
      const newRings = [];
      const zero = new Point(0, 0);
      for (let k = 0; k < rings.length; k++) {
          const ring = rings[k];
          const newRing = [];
          for (let i = 0; i < ring.length; i++) {
              const a = ring[i - 1];
              const b = ring[i];
              const c = ring[i + 1];
              const aToB = i === 0 ? zero : b.sub(a)._unit()._perp();
              const bToC = i === ring.length - 1 ? zero : c.sub(b)._unit()._perp();
              const extrude = aToB._add(bToC)._unit();
  
              const cosHalfAngle = extrude.x * bToC.x + extrude.y * bToC.y;
              extrude._mult(1 / cosHalfAngle);
  
              newRing.push(extrude._mult(offset)._add(b));
          }
          newRings.push(newRing);
      }
      return newRings;
  }
  