2021-05-26 17:41:35 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Onebox
|
|
|
|
module Engine
|
|
|
|
class GoogleMapsOnebox
|
|
|
|
include Engine
|
|
|
|
|
|
|
|
class << self
|
|
|
|
def ===(other)
|
|
|
|
if other.kind_of? URI
|
|
|
|
@@matchers && @@matchers.any? { |m| other.to_s =~ m[:regexp] }
|
|
|
|
else
|
|
|
|
super
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def matches_regexp(key, regexp)
|
|
|
|
(@@matchers ||= []) << { key: key, regexp: regexp }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
always_https
|
|
|
|
requires_iframe_origins("https://maps.google.com", "https://google.com")
|
|
|
|
|
|
|
|
# Matches shortened Google Maps URLs
|
|
|
|
matches_regexp :short, %r{^(https?:)?//goo\.gl/maps/}
|
|
|
|
|
|
|
|
# Matches URLs for custom-created maps
|
|
|
|
matches_regexp :custom,
|
|
|
|
%r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps/d/(?:edit|viewer|embed)\?mid=.+$"
|
|
|
|
|
|
|
|
# Matches URLs with streetview data
|
|
|
|
matches_regexp :streetview,
|
|
|
|
%r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps[^@]+@(?<lon>-?[\d.]+),(?<lat>-?[\d.]+),(?:\d+)a,(?<zoom>[\d.]+)y,(?<heading>[\d.]+)h,(?<pitch>[\d.]+)t.+?data=.*?!1s(?<pano>[^!]{22})"
|
|
|
|
|
|
|
|
# Matches "normal" Google Maps URLs with arbitrary data
|
|
|
|
matches_regexp :standard, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps"
|
|
|
|
|
|
|
|
# Matches URLs for the old Google Maps domain which we occasionally get redirected to
|
|
|
|
matches_regexp :canonical, %r"^(?:https?:)?//maps\.google(?:\.(?:\w{2,}))+/maps\?"
|
|
|
|
|
|
|
|
def initialize(url, timeout = nil)
|
|
|
|
super
|
|
|
|
resolve_url!
|
|
|
|
rescue Net::HTTPServerException,
|
|
|
|
Timeout::Error,
|
|
|
|
Net::HTTPError,
|
|
|
|
Errno::ECONNREFUSED,
|
|
|
|
RuntimeError => err
|
|
|
|
raise ArgumentError, "malformed url or unresolveable: #{err.message}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def streetview?
|
|
|
|
!!@streetview
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_html
|
|
|
|
"<div class='maps-onebox'><iframe src=\"#{link}\" width=\"690\" height=\"400\" frameborder=\"0\" style=\"border:0\">#{placeholder_html}</iframe></div>"
|
|
|
|
end
|
|
|
|
|
|
|
|
def placeholder_html
|
|
|
|
::Onebox::Helpers.map_placeholder_html
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def data
|
|
|
|
{ link: url, title: url }
|
|
|
|
end
|
|
|
|
|
|
|
|
def resolve_url!
|
|
|
|
@streetview = false
|
|
|
|
type, match = match_url
|
|
|
|
|
|
|
|
# Resolve shortened URL, if necessary
|
|
|
|
if type == :short
|
|
|
|
follow_redirect!
|
|
|
|
type, match = match_url
|
|
|
|
end
|
|
|
|
|
|
|
|
# Try to get the old-maps URI, it is far easier to embed.
|
|
|
|
if type == :standard
|
|
|
|
retry_count = 10
|
|
|
|
while (retry_count -= 1) > 0
|
|
|
|
follow_redirect!
|
|
|
|
type, match = match_url
|
|
|
|
break if type != :standard
|
|
|
|
sleep 0.1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
case type
|
|
|
|
when :standard
|
|
|
|
# Fallback for map URLs that don't resolve into an easily embeddable old-style URI
|
|
|
|
# Roadmaps use a "z" zoomlevel, satellite maps use "m" the horizontal width in meters
|
|
|
|
# TODO: tilted satellite maps using "a,y,t"
|
2023-03-01 19:20:38 +08:00
|
|
|
match = @url.match(/@(?<lon>[\d.-]+),(?<lat>[\d.-]+),(?<zoom>\d+)(\.\d+)?(?<mz>[mz])/)
|
2021-05-26 17:41:35 +08:00
|
|
|
raise "unexpected standard url #{@url}" unless match
|
|
|
|
zoom = match[:mz] == "z" ? match[:zoom] : Math.log2(57280048.0 / match[:zoom].to_f).round
|
|
|
|
location = "#{match[:lon]},#{match[:lat]}"
|
|
|
|
url = "https://maps.google.com/maps?ll=#{location}&z=#{zoom}&output=embed&dg=ntvb"
|
|
|
|
url += "&q=#{$1}" if match = @url.match(%r{/place/([^/\?]+)})
|
|
|
|
url += "&cid=#{($1 + $2).to_i(16)}" if @url.match(/!3m1!1s0x(\h{16}):0x(\h{16})/)
|
|
|
|
@url = url
|
|
|
|
@placeholder =
|
|
|
|
"https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap¢er=#{location}&zoom=#{zoom}&size=690x400&sensor=false"
|
|
|
|
when :custom
|
|
|
|
url = @url.dup
|
|
|
|
@url = rewrite_custom_url(url, "embed")
|
|
|
|
@placeholder = rewrite_custom_url(url, "thumbnail")
|
|
|
|
@placeholder_height = @placeholder_width = 120
|
|
|
|
when :streetview
|
|
|
|
@streetview = true
|
|
|
|
panoid = match[:pano]
|
|
|
|
lon = match[:lon].to_f.to_s
|
|
|
|
lat = match[:lat].to_f.to_s
|
|
|
|
heading = match[:heading].to_f.round(4).to_s
|
|
|
|
pitch = (match[:pitch].to_f / 10.0).round(4).to_s
|
|
|
|
fov = (match[:zoom].to_f / 100.0).round(4).to_s
|
|
|
|
zoom = match[:zoom].to_f.round
|
|
|
|
@url =
|
|
|
|
"https://www.google.com/maps/embed?pb=!3m2!2sen!4v0!6m8!1m7!1s#{panoid}!2m2!1d#{lon}!2d#{lat}!3f#{heading}!4f#{pitch}!5f#{fov}"
|
|
|
|
@placeholder =
|
|
|
|
"https://maps.googleapis.com/maps/api/streetview?size=690x400&location=#{lon},#{lat}&pano=#{panoid}&fov=#{zoom}&heading=#{heading}&pitch=#{pitch}&sensor=false"
|
|
|
|
when :canonical
|
|
|
|
query = URI.decode_www_form(uri.query).to_h
|
|
|
|
if !query.has_key?("ll")
|
|
|
|
unless query.has_key?("sll")
|
|
|
|
raise ArgumentError, "canonical url lacks location argument"
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
query["ll"] = query["sll"]
|
|
|
|
@url += "&ll=#{query["sll"]}"
|
|
|
|
end
|
|
|
|
location = query["ll"]
|
|
|
|
if !query.has_key?("z")
|
|
|
|
unless query.has_key?("spn") || query.has_key?("sspn")
|
|
|
|
raise ArgumentError, "canonical url has incomplete query arguments"
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
if !query.has_key?("spn")
|
|
|
|
query["spn"] = query["sspn"]
|
|
|
|
@url += "&spn=#{query["sspn"]}"
|
|
|
|
end
|
|
|
|
angle = query["spn"].split(",").first.to_f
|
|
|
|
zoom = (Math.log(690.0 * 360.0 / angle / 256.0) / Math.log(2)).round
|
|
|
|
else
|
|
|
|
zoom = query["z"]
|
|
|
|
end
|
|
|
|
@url = @url.sub("output=classic", "output=embed")
|
|
|
|
@placeholder =
|
|
|
|
"https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&size=690x400&sensor=false¢er=#{location}&zoom=#{zoom}"
|
|
|
|
else
|
|
|
|
raise "unexpected url type #{type.inspect}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def match_url
|
|
|
|
@@matchers.each do |matcher|
|
|
|
|
if m = matcher[:regexp].match(@url)
|
|
|
|
return matcher[:key], m
|
|
|
|
end
|
|
|
|
end
|
|
|
|
raise ArgumentError, "\"#{@url}\" does not match any known pattern"
|
|
|
|
end
|
|
|
|
|
|
|
|
def rewrite_custom_url(url, target)
|
|
|
|
uri = URI(url)
|
|
|
|
uri.path = uri.path.sub(%r{(?<=^/maps/d/)\w+$}, target)
|
|
|
|
uri.to_s
|
|
|
|
end
|
|
|
|
|
|
|
|
def follow_redirect!
|
|
|
|
begin
|
2022-11-02 00:33:17 +08:00
|
|
|
http =
|
|
|
|
FinalDestination::HTTP.start(
|
2021-05-26 17:41:35 +08:00
|
|
|
uri.host,
|
|
|
|
uri.port,
|
|
|
|
use_ssl: uri.scheme == "https",
|
|
|
|
open_timeout: timeout,
|
|
|
|
read_timeout: timeout,
|
|
|
|
)
|
|
|
|
|
|
|
|
response = http.head(uri.path)
|
|
|
|
unless %w[200 301 302].include?(response.code)
|
|
|
|
raise "unexpected response code #{response.code}"
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
|
|
|
|
@url = response.code == "200" ? uri.to_s : response["Location"]
|
|
|
|
@uri = URI(@url)
|
|
|
|
ensure
|
2023-01-09 20:10:19 +08:00
|
|
|
begin
|
2021-05-26 17:41:35 +08:00
|
|
|
http.finish
|
|
|
|
rescue StandardError
|
|
|
|
nil
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2021-05-26 17:41:35 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|