publiclab/mapknitter

View on GitHub
app/models/map.rb

Summary

Maintainability
C
1 day
Test Coverage
class Map < ApplicationRecord
  include ActiveModel::Validations
  extend FriendlyId
  friendly_id :name, use: %i(slugged static)

  module Status
    VALUES = [
      BANNED = 0,    # Usage: Status::BANNED
      NORMAL = 1,    # Usage: Status::NORMAL
      MODERATED = 4, # Usage: Status::MODERATED
    ].freeze
  end

  attr_accessor :image_urls

  validates_presence_of :name, :slug, :author, :lat, :lon
  validates_uniqueness_of :slug
  validates_presence_of :location, message: ' cannot be found. Try entering a latitude and longitude if this problem persists.'
  # validates_format_of   :slug,
  #                       :with => /^[\w-]*$/,
  #                       :message => " must not include spaces and must be alphanumeric, as it'll be used in the URL of your map, like: https://mapknitter.org/maps/your-map-name. You may use dashes and underscores.",
  #                       :on => :create
  # validates_format_of :tile_url, :with => /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/ix
  validates_with NotAtOriginValidator
  validates :lat, :lon, NotAtOrigin: true

  has_many :exports, dependent: :destroy
  has_many :tags, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :annotations, dependent: :destroy
  belongs_to :user, optional: true

  has_many :warpables
  scope :active, -> { where(status: Status::NORMAL) }
  scope :has_user, -> { where('user_id != ?', 0) }

  def validate
    lat >= -90 && lat <= 90 && lon >= -180 && lat <= 180 if name != 'untitled'
  end

  # Hash the password before saving the record.
  def before_create
    self.password = Password.update(password) if password != ""
  end

  def placed_warpables
    warpables.where('width > 0 AND nodes <> ""')
  end

  def private
    password != ""
  end

  def anonymous?
    author == "anonymous" || user_id.zero?
  end

  def self.anonymous
    Map.active.where(user_id: 0)
  end

  def self.bbox(minlat, minlon, maxlat, maxlon, tag = nil)
    if tag.nil?
      Map.active.where(
        ['lat > ? AND lat < ? AND lon > ? AND lon < ?',
         minlat, maxlat, minlon, maxlon,]
      )
    else
      Map.active.where(
        ['lat > ? AND lat < ? AND lon > ? AND lon < ?',
         minlat, maxlat, minlon, maxlon,]
      ).joins(:tags).where("tags.name = ?", tag)
    end
  end

  def exporting?
    export&.running?
  end

  def export
    latest_export
  end

  def latest_export
    exports.last
  end

  def self.authors(limit = 50)
    Map.where(status: Status::NORMAL, password: '')
       .limit(limit)
       .order("maps.id DESC")
       .collect(&:author)
  end

  def self.search(query)
    query = query.squeeze(' ').strip
    Map.active.where([
      'author LIKE ? OR name LIKE ? OR location LIKE ? OR description LIKE ?',
      "%#{query}%", "%#{query}%", "%#{query}%", "%#{query}%",
    ])
  end

  def self.featured
    Map.active.joins(:warpables)
       .select('maps.*, count(maps.id) as image_count')
       .group('warpables.map_id')
       .order('image_count DESC')
  end

  def self.new_maps
    Map.where(status: Status::NORMAL, password: '')
      .order('created_at DESC')
      .limit(12)
  end

  def self.map
    Map.where(status: Status::NORMAL, password: '')
       .select('author, maps.name, lat, lon, slug, archived, maps.status as map_status,
        password, users.login as user_login')
       .joins(:warpables, :user)
       .group('maps.id')
  end

  def self.featured_authors
    maps = Map.active.has_user

    author_counts = maps.select('user_id, author, count(1) as maps_count')
                        .group('author')
                        .order('maps_count DESC')

    author_counts.map do |a|
      user = User.find(a.user_id)
      { user: user, count: a.maps_count, location: user.maps.first.location } if user.status = Status::NORMAL
    end
  end

  def self.maps_nearby(lat:, lon:, dist:)
    Map.active.where([
      'lat>? AND lat<? AND lon>? AND lon<?',
      lat - dist, lat + dist, lon - dist, lon + dist,
    ])
  end

  def nodes
    nodes = {}
    warpables.each do |warpable|
      if warpable.nodes
        w_nodes = []
        warpable.nodes.split(',').each do |node|
          node_obj = Node.find(node)
          w_nodes << [node_obj.lon, node_obj.lat]
        end
        nodes[warpable.id.to_s] = w_nodes
      end
      nodes[warpable.id.to_s] ||= 'none'
    end
    nodes
  end

  # find all other maps within <dist> degrees lat or lon
  def nearby_maps(dist)
    return [] if lat.to_f == 0.0 || lon.to_f == 0.0

    Map.active.where('id != ? AND lat > ? AND lat < ? AND lon > ? AND lon < ?',
      id, lat - dist, lat + dist, lon - dist, lon + dist)
      .limit(10)
  end

  def average_scale
    # determine optimal zoom level
    puts '> calculating scale'
    pxperms = []
    placed_warpables.each do |warpable|
      pxperms << 100.00 / warpable.cm_per_pixel if warpable.placed?
    end
    pxperms.sum / pxperms.length
  end

  def best_cm_per_pixel
    hist = images_histogram
    scores = []
    (0..(hist.length - 1)).each do |i|
      scores[i] = 0
      scores[i] += hist[i - 3] if i > 3
      scores[i] += hist[i - 2] if i > 2
      scores[i] += hist[i - 1] if i > 1
      scores[i] += hist[i]
      scores[i] += hist[i + 1] if i < hist.length - 2
      scores[i] += hist[i + 2] if i < hist.length - 3
      scores[i] += hist[i + 3] if i < hist.length - 4
    end
    highest = 0
    scores.each_with_index { |s, i| highest = i if s > scores[highest] }
    highest
  end

  def average_cm_per_pixel
    if !warpables.empty?
      scales = []
      count = 0
      average = 0
      placed_warpables.each do |warpable|
        count += 1
        res = warpable.cm_per_pixel
        res = 1 if res.zero? # let's not ever try to go for infinite resolution
        scales << res unless res.nil?
      end
      total_sum = scales.sum unless scales.empty?
      average = total_sum / count if total_sum
      average
    else
      0
    end
  end

  # for sparklines graph display
  def images_histogram
    hist = []
    warpables.each do |warpable|
      res = warpable.cm_per_pixel.to_i
      hist[res] = 0 if hist[res].nil?
      hist[res] += 1
    end
    (0..hist.length - 1).each do |bin|
      hist[bin] = 0 if hist[bin].nil?
    end
    hist
  end

  # for sparklines graph display
  def grouped_images_histogram(binsize)
    hist = []
    warpables.each do |warpable|
      res = warpable.cm_per_pixel
      next if res.nil?

      res = (warpable.cm_per_pixel / (0.001 + binsize)).to_i
      hist[res] = 0 if hist[res].nil?
      hist[res] += 1
    end
    (0..hist.length - 1).each do |bin|
      hist[bin] = 0 if hist[bin].nil?
    end
    hist
  end

  # we'll eventually replace this with a JavaScript call to initiate an external export process:
  def run_export(user, resolution)
    key = APP_CONFIG ? APP_CONFIG["google_maps_api_key"] : "AIzaSyAOLUQngEmJv0_zcG1xkGq-CXIPpLQY8iQ"

    new_export = Export.new(map_id: id) unless export

    Exporter.run_export(user,
      resolution,
      export || new_export,
      id,
      slug,
      Rails.root.to_s,
      average_scale,
      placed_warpables,
      key)
  end

  def after_create
    puts 'saving Map'
    return unless Map.find_by_name(slug, order: "version DESC")

    self.version = last.version + 1
  end

  def license_link
    if license == "cc-by"
      "<a href='http://creativecommons.org/licenses/by/3.0/'>Creative Commons Attribution 3.0 Unported License</a>"
    elsif license == "publicdomain"
      "<a href='http://creativecommons.org/publicdomain/zero/1.0/'>Public Domain</a>"
    end
  end

  def has_tag(tagname)
    !Tag.where(map_id: id, name: tagname).empty?
  end

  def add_tag(tagname, user)
    tagname = tagname.downcase
    tags.create(name: tagname, user_id: user.id, map_id: id) unless has_tag(tagname)
  end

  def fetch_map_data
    # fetches a list of updated warpables along with their corners in a json format.
    data = warpables
    data.to_json
  end

  def authors
    user_ids = []
    warpables.each do |warp|
      user_ids.push(warp.versions.map(&:whodunnit))
    end
    User.where(id: user_ids.flatten.uniq).where.not(id: user_id)
  end

  def spam
    update!(status: Status::BANNED)
  end

  def publish
    update!(status: Status::NORMAL)
  end
end