diff --git a/Gemfile.lock b/Gemfile.lock index c3b1cffdd..435fd292b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -493,7 +493,7 @@ GEM sprockets (> 3.0) sprockets-rails tilt - scout_apm (2.6.3) + scout_apm (2.6.4) parser searchkick (4.1.0) activemodel (>= 5) diff --git a/app/assets/images/spade-marker.svg b/app/assets/images/spade-marker.svg new file mode 100644 index 000000000..791a3709e --- /dev/null +++ b/app/assets/images/spade-marker.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/assets/javascripts/crops.js.erb b/app/assets/javascripts/cropmap.js.erb similarity index 91% rename from app/assets/javascripts/crops.js.erb rename to app/assets/javascripts/cropmap.js.erb index 78a3a2222..756530666 100644 --- a/app/assets/javascripts/crops.js.erb +++ b/app/assets/javascripts/cropmap.js.erb @@ -9,12 +9,20 @@ function showCropMap(cropmap) { }).addTo(cropmap); var markers = new L.MarkerClusterGroup({showCoverageOnHover: false, maxClusterRadius: 20 }); + var crop_icon = L.icon({ + iconUrl: location.pathname + '.svg', + iconSize: [24, 24], + iconAnchor: [12, 12], + popupAnchor: [0, -12], + }); + var things_to_map = location.pathname + '.json'; $.getJSON(things_to_map, function(crop) { $.each(crop.plantings, function(i, planting) { var owner = planting.owner; if (owner.latitude && owner.longitude) { - var marker = new L.Marker(new L.LatLng(owner.latitude, owner.longitude)); + var marker = new L.Marker(new L.LatLng(owner.latitude, owner.longitude), + {'icon': crop_icon}); var planting_url = "/plantings/" + planting.id; var planting_link = "" + owner.login_name + "'s " + crop.name + ""; @@ -44,7 +52,6 @@ function showCropMap(cropmap) { $(document).ready(function() { if (document.getElementById("cropmap") !== null) { - L.Icon.Default.imagePath = '/assets'; var cropmap = L.map('cropmap').setView([0.0, -0.0], 2); diff --git a/app/assets/javascripts/members.js.erb b/app/assets/javascripts/members.js.erb index 51a662758..42fafe6b5 100644 --- a/app/assets/javascripts/members.js.erb +++ b/app/assets/javascripts/members.js.erb @@ -7,6 +7,13 @@ if (document.getElementById("membermap") !== null) { L.Icon.Default.imagePath = '/assets'; + var default_marker_icon = L.icon({ + iconUrl: "<%=image_url('spade-marker.svg')%>", + iconSize: [48, 48], + iconAnchor: [24, 48], + popupAnchor: [0, -46], + }); + $.getJSON(location.pathname + '.json', function(member) { if (member.latitude && member.longitude) { var membermap = L.map('membermap').setView([member.latitude, member.longitude], 4); @@ -15,7 +22,8 @@ if (document.getElementById("membermap") !== null) { attribution: 'Map data © OpenStreetMap contributors under ODbL | Map imagery © Mapbox', maxZoom: 18 }).addTo(membermap); - var marker = new L.Marker(new L.LatLng(member.latitude, member.longitude)); + var marker = new L.Marker(new L.LatLng(member.latitude, member.longitude), + {'icon': default_marker_icon}); var member_url = "/members/" + member.slug; var member_link = "" + member.login_name + ""; diff --git a/app/assets/javascripts/places.js.erb b/app/assets/javascripts/places.js.erb index f0322b38b..b5917f614 100644 --- a/app/assets/javascripts/places.js.erb +++ b/app/assets/javascripts/places.js.erb @@ -1,20 +1,23 @@ if (document.getElementById("placesmap") !== null) { - places_base_path = "/places"; - mapbox_map_id = "<%= Rails.env == 'test' ? 0 : Rails.application.config.mapbox_map_id %>"; - mapbox_access_token = "<%= Rails.env == 'test' ? 0 : Rails.application.config.mapbox_access_token %>"; - mapbox_base_url = "https://a.tiles.mapbox.com/v4/" + mapbox_map_id + "/{z}/{x}/{y}.png?access_token=" + mapbox_access_token; - nominatim_base_url = 'https://nominatim.openstreetmap.org/search/'; - nominatim_user_agent_email = "<%= Rails.env == 'test' ? 0 : Rails.application.config.user_agent_email %>"; + var places_base_path = "/places"; + var mapbox_map_id = "<%= Rails.env == 'test' ? 0 : Rails.application.config.mapbox_map_id %>"; + var mapbox_access_token = "<%= Rails.env == 'test' ? 0 : Rails.application.config.mapbox_access_token %>"; + var mapbox_base_url = "https://a.tiles.mapbox.com/v4/" + mapbox_map_id + "/{z}/{x}/{y}.png?access_token=" + mapbox_access_token; + var nominatim_base_url = 'https://nominatim.openstreetmap.org/search/'; + var nominatim_user_agent_email = "<%= Rails.env == 'test' ? 0 : Rails.application.config.user_agent_email %>"; L.Icon.Default.imagePath = '/assets' - if (location.pathname === places_base_path) { //places index page + var placesmap; + + if (location.pathname === places_base_path || location.pathname === '/') { //places index page placesmap = L.map('placesmap').setView([0.0, -0.0], 2); showMap(placesmap); - } else { // specific place page - place = location.pathname.replace(places_base_path + "/", ''); - nominatim_query_url = nominatim_base_url + place; - nominatim_options = { + } + else { // specific place page + var place = location.pathname.replace(places_base_path + "/", ''); + var nominatim_query_url = nominatim_base_url + place; + var nominatim_options = { format: "json", callback: "placeholder", limit: 1, @@ -32,20 +35,58 @@ function showMap(placesmap) { attribution: 'Map data © OpenStreetMap contributors under ODbL | Map imagery © Mapbox', maxZoom: 18 }).addTo(placesmap); - markers = new L.MarkerClusterGroup({showCoverageOnHover: false, maxClusterRadius: 20 }); - things_to_map = location.pathname + '.json'; - $.getJSON(things_to_map, function(members) { + var markers = new L.MarkerClusterGroup({showCoverageOnHover: false, maxClusterRadius: 20 }); + + var members_url = location.pathname + '.json'; + + // Icon documentation: https://leafletjs.com/reference-1.5.0.html#icon + var default_marker_icon = L.icon({ + iconUrl: "<%=image_url('spade-marker.svg')%>", + iconSize: [48, 48], + iconAnchor: [24, 48], + popupAnchor: [0, -46], + }); + + $.getJSON(members_url, function(members) { $.each(members, function(i, m) { if (m.latitude && m.longitude) { - marker = new L.Marker(new L.LatLng(m.latitude, m.longitude)); - link = "

" + m.login_name + "

"; - where = "

" + m.location + "

"; + var marker = new L.Marker(new L.LatLng(m.latitude, m.longitude), {icon: default_marker_icon}); + var link = "

" + m.login_name + "

"; + var where = "

" + m.location + "

"; marker.bindPopup(link + where).openPopup(); markers.addLayer(marker); } }); }); + function fetchCropsForMap(url) { + $.getJSON(url, function(crop_data) { + $.each(crop_data['data'], function(i, p) { + if (p.attributes.latitude && p.attributes.longitude) { + var crop_marker = L.icon({ + iconUrl: "/crops/" + p.attributes['crop-slug'] + '.svg', + iconSize: [48, 48], + iconAnchor: [24, 48], + popupAnchor: [0, -46], + }); + var marker = new L.Marker(new L.LatLng(p.attributes.latitude, p.attributes.longitude), + {icon: crop_marker}); + var link = "

" + p.attributes['crop-name'] + "

"; + var where = "

" + p.attributes.location + "

"; + if (p.attributes['thumbnail']) { + where += 'photo of planting'; + } + marker.bindPopup(link + where).openPopup(); + markers.addLayer(marker); + } + }); + if (crop_data['links']['next']) { + fetchCropsForMap(crop_data['links']['next']) + } + return crop_data; + }); + } + fetchCropsForMap('/api/v1/plantings.json?filter[finished]=false&sort=-planted-at'); placesmap.addLayer(markers); } diff --git a/app/assets/stylesheets/leaflet_overrides.scss b/app/assets/stylesheets/leaflet_overrides.scss index aa69bb297..c89f27303 100644 --- a/app/assets/stylesheets/leaflet_overrides.scss +++ b/app/assets/stylesheets/leaflet_overrides.scss @@ -1,13 +1,13 @@ .leaflet-popup-content-wrapper, .leaflet-popup-tip { - border: none; + border: 0; } .thumbnail { - background: #fff !important; - border: solid 1px whitesmoke; -} + background: $white; + border: solid 1px $white; -.thumbnail .crop-thumbnail .cropinfo { - padding-top: 14px; + .crop-thumbnail .cropinfo { + padding-top: 14px; + } } diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 2dd6a948c..7aefd98af 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -66,7 +66,10 @@ class CropsController < ApplicationController respond_to do |format| format.html - format.svg { send_data(@crop.svg_icon, type: "image/svg+xml", disposition: "inline") } + format.svg do + icon_data = @crop.svg_icon.presence || File.read(Rails.root.join('app', 'assets', 'images', 'icons', 'sprout.svg')) + send_data(icon_data, type: "image/svg+xml", disposition: "inline") + end format.json { render json: @crop.to_json(crop_json_fields) } end end diff --git a/app/models/concerns/open_farm_data.rb b/app/models/concerns/open_farm_data.rb index f56c0fa66..077971574 100644 --- a/app/models/concerns/open_farm_data.rb +++ b/app/models/concerns/open_farm_data.rb @@ -19,7 +19,10 @@ module OpenFarmData end def svg_icon - fetch_attr('svg_icon') + icon = fetch_attr('svg_icon') + return icon if icon.present? + + parent.svg_icon if parent.present? end def tags_array diff --git a/app/models/planting.rb b/app/models/planting.rb index 868f36735..686153b64 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -45,10 +45,12 @@ class Planting < ApplicationRecord ## ## Delegations - delegate :name, :en_wikipedia_url, :default_scientific_name, :plantings_count, + delegate :name, :slug, :en_wikipedia_url, :default_scientific_name, :plantings_count, to: :crop, prefix: true - delegate :annual?, to: :crop + delegate :annual?, :svg_icon, to: :crop + delegate :location, :longitude, :latitude, to: :garden + ## ## Validations validates :garden, presence: true @@ -77,11 +79,6 @@ class Planting < ApplicationRecord ].join('-').tr(' ', '-').downcase end - # location = garden owner + garden name, i.e. "Skud's backyard" - def location - I18n.t("gardens.location", garden: garden.name, owner: garden.owner.login_name) - end - # stringify as "beet in Skud's backyard" or similar def to_s I18n.t('plantings.string', crop: crop.name, garden: garden.name, owner: owner) diff --git a/app/resources/planting_resource.rb b/app/resources/planting_resource.rb index 1c641c8e7..629e8e9a6 100644 --- a/app/resources/planting_resource.rb +++ b/app/resources/planting_resource.rb @@ -3,17 +3,26 @@ class PlantingResource < BaseResource has_one :garden has_one :crop - # has_one :owner, class_name: 'Member' + has_one :owner, class_name: 'Member' has_many :photos has_many :harvests - attribute :slug, :planted_at, :finished, :finished_at, :quantity, :description, :sunniness, :planted_from + attribute :slug + attribute :planted_at + attribute :finished + attribute :finished_at + attribute :quantity + attribute :description + attribute :sunniness + attribute :planted_from # Predictions - # attribute :expected_lifespan - # attribute :finish_predicted_at - # attribute :first_harvest_date - # attribute :last_harvest_date + attribute :expected_lifespan + attribute :finish_predicted_at + attribute :first_harvest_date + attribute :last_harvest_date + + attributes :longitude, :latitude, :location filter :slug filter :crop @@ -22,18 +31,16 @@ class PlantingResource < BaseResource filter :owner filter :finished - # attribute :percentage_grown - # def percentage_grown - # @model.percentage_grown - # end + attribute :percentage_grown + def percentage_grown + @model.percentage_grown + end - # attribute :crop_name - # def crop_name - # @model.crop.name - # end + attribute :crop_name + attribute :crop_slug - # attribute :thumbnail - # def thumbnail - # @model.default_photo&.thumbnail_url - # end + attribute :thumbnail + def thumbnail + @model.default_photo&.thumbnail_url + end end diff --git a/app/views/places/index.html.haml b/app/views/places/index.html.haml index ebc180a81..f915e67db 100644 --- a/app/views/places/index.html.haml +++ b/app/views/places/index.html.haml @@ -1,3 +1,6 @@ - content_for :title, t(".title", site_name: ENV['GROWSTUFF_SITE_NAME']) + +%h2=t(".title", site_name: ENV['GROWSTUFF_SITE_NAME']) + = render partial: 'search_form' #placesmap.map diff --git a/spec/controllers/api/v1/plantings_controller_spec.rb b/spec/controllers/api/v1/plantings_controller_spec.rb index c187e1213..b807c724e 100644 --- a/spec/controllers/api/v1/plantings_controller_spec.rb +++ b/spec/controllers/api/v1/plantings_controller_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Api::V1::PlantingsController, type: :controller do let(:expected_attributes) do { 'crop-name' => my_planting.crop.name, + 'crop-slug' => my_planting.crop.slug, 'description' => my_planting.description, 'expected-lifespan' => nil, 'finish-predicted-at' => nil, @@ -23,6 +24,9 @@ RSpec.describe Api::V1::PlantingsController, type: :controller do 'finished-at' => my_planting.finished_at, 'first-harvest-date' => nil, 'last-harvest-date' => nil, + 'latitude' => my_planting.garden.latitude, + 'longitude' => my_planting.garden.longitude, + 'location' => my_planting.garden.location, 'percentage-grown' => nil, 'planted-at' => '2000-01-01', 'planted-from' => my_planting.planted_from, @@ -46,6 +50,7 @@ RSpec.describe Api::V1::PlantingsController, type: :controller do let(:expected_attributes) do { 'crop-name' => my_planting.crop.name, + 'crop-slug' => my_planting.crop.slug, 'description' => my_planting.description, 'expected-lifespan' => nil, 'finish-predicted-at' => nil, @@ -53,6 +58,9 @@ RSpec.describe Api::V1::PlantingsController, type: :controller do 'finished-at' => my_planting.finished_at, 'first-harvest-date' => nil, 'last-harvest-date' => nil, + 'latitude' => my_planting.garden.latitude, + 'longitude' => my_planting.garden.longitude, + 'location' => my_planting.garden.location, 'percentage-grown' => nil, 'planted-at' => '2000-01-01', 'planted-from' => my_planting.planted_from, diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index 56e477f4f..3bbd0a255 100644 --- a/spec/models/planting_spec.rb +++ b/spec/models/planting_spec.rb @@ -212,7 +212,7 @@ describe Planting do end it "generates a location" do - planting.location.should eq "#{garden_owner.login_name}'s #{garden.name}" + planting.location.should eq garden.location end it "has a slug" do diff --git a/spec/requests/api/v1/plantings_request_spec.rb b/spec/requests/api/v1/plantings_request_spec.rb index cfd60fcf7..705c23c6f 100644 --- a/spec/requests/api/v1/plantings_request_spec.rb +++ b/spec/requests/api/v1/plantings_request_spec.rb @@ -4,7 +4,7 @@ RSpec.describe 'Plantings', type: :request do subject { JSON.parse response.body } let(:headers) { { 'Accept' => 'application/vnd.api+json' } } - let!(:planting) { FactoryBot.create :planting } + let!(:planting) { FactoryBot.create :planting } let(:planting_encoded_as_json_api) do { "id" => planting.id.to_s, "type" => "plantings", @@ -60,6 +60,7 @@ RSpec.describe 'Plantings', type: :request do "quantity" => 33, "description" => planting.description, "crop-name" => planting.crop.name, + "crop-slug" => planting.crop.slug, "sunniness" => nil, "planted-from" => nil, "expected-lifespan" => nil, @@ -67,12 +68,17 @@ RSpec.describe 'Plantings', type: :request do "percentage-grown" => nil, "first-harvest-date" => nil, "last-harvest-date" => nil, - "thumbnail" => nil + "thumbnail" => nil, + "location" => planting.garden.location, + "longitude" => planting.garden.longitude, + "latitude" => planting.garden.latitude } end it '#index' do get '/api/v1/plantings', params: {}, headers: headers + expect(subject['data'][0].keys).to eq(planting_encoded_as_json_api.keys) + expect(subject['data'][0]['attributes'].keys.sort!).to eq(planting_encoded_as_json_api['attributes'].keys.sort!) expect(subject['data']).to include(planting_encoded_as_json_api) end