Skip to content

Commit

Permalink
react/kiezradar: add kiezradars on projects overview
Browse files Browse the repository at this point in the history
  • Loading branch information
sevfurneaux committed Feb 8, 2025
1 parent a729e86 commit 2f158e8
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 32 deletions.
4 changes: 4 additions & 0 deletions meinberlin/apps/kiezradar/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ def update(self, instance, validated_data):

def to_representation(self, instance):
representation = super().to_representation(instance)
if instance.kiezradar:
representation["kiezradar"] = KiezRadarSerializer(instance.kiezradar).data
else:
representation["kiezradar"] = None
representation["organisations"] = [
{"id": organisation.id, "name": organisation.name}
for organisation in instance.organisations.all()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
data-use_vector_map="{{ use_vector_map }}"
data-baseurl="{{ baseurl }}"
data-bounds="{{ bounds }}"
data-kiezradars="{{ kiezradars }}"
data-search-profile="{{ search_profile|default:"" }}"
data-search-profiles-url="{{ search_profiles_url }}"
data-search-profiles-count="{{ search_profiles_count }}"
Expand Down
14 changes: 14 additions & 0 deletions meinberlin/apps/plans/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
from meinberlin.apps.contrib.enums import TopicEnum
from meinberlin.apps.contrib.views import CanonicalURLDetailView
from meinberlin.apps.dashboard.mixins import DashboardProjectListGroupMixin
from meinberlin.apps.kiezradar.models import KiezRadar
from meinberlin.apps.kiezradar.models import ProjectStatus
from meinberlin.apps.kiezradar.models import ProjectType
from meinberlin.apps.kiezradar.models import SearchProfile
from meinberlin.apps.kiezradar.serializers import KiezRadarSerializer
from meinberlin.apps.kiezradar.serializers import SearchProfileSerializer
from meinberlin.apps.maps.models import MapPreset
from meinberlin.apps.organisations.models import Organisation
Expand Down Expand Up @@ -133,6 +135,17 @@ def get_project_status(self):
]
return json.dumps(statuses)

def get_kiezradars(self):
if not self.request.user.is_authenticated:
return json.dumps([])

kiezradars = KiezRadar.objects.filter(creator=self.request.user)
return (
JSONRenderer()
.render(KiezRadarSerializer(kiezradars, many=True).data)
.decode("utf-8")
)

def get_search_profile(self):
if (
self.request.GET.get("search-profile", None)
Expand Down Expand Up @@ -174,6 +187,7 @@ def get_context_data(self, **kwargs):
if hasattr(settings, "A4_OPENMAPTILES_TOKEN"):
omt_token = settings.A4_OPENMAPTILES_TOKEN

context["kiezradars"] = self.get_kiezradars()
context["search_profile"] = self.get_search_profile()
context["districts"] = self.get_districts()
context["organisations"] = self.get_organisations()
Expand Down
1 change: 1 addition & 0 deletions meinberlin/react/kiezradar/SearchProfile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default function SearchProfile ({ apiUrl, planListUrl, profile: profile_,

const selection = [
[profile.query_text],
[profile.kiezradar ? profile.kiezradar.name : null],
...filters,
[profile.plans_only ? plansText : null]
]
Expand Down
1 change: 1 addition & 0 deletions meinberlin/react/kiezradar/use-create-search-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function useCreateSearchProfile ({
project_types: participationIds,
status: projectStatusIds,
plans_only: appliedFilters.plansOnly,
kiezradar: appliedFilters.kiezradar?.id,
notification: true
}

Expand Down
3 changes: 1 addition & 2 deletions meinberlin/react/plans/SaveSearchProfile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const savingText = django.gettext('Saving')
export default function SaveSearchProfile ({
isAuthenticated,
searchProfile,
searchProfilesCount,
...props
}) {
if (!isAuthenticated) {
Expand All @@ -38,7 +37,7 @@ export default function SaveSearchProfile ({
)
}

return <CreateSearchProfileButton {...props} searchProfilesCount={searchProfilesCount} />
return <CreateSearchProfileButton {...props} />
}

function CreateSearchProfileButton ({
Expand Down
2 changes: 2 additions & 0 deletions meinberlin/react/plans/react_plans_map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function init () {
const attribution = el.getAttribute('data-attribution')
const baseUrl = el.getAttribute('data-baseurl')
const bounds = JSON.parse(el.getAttribute('data-bounds'))
const kiezradars = el.getAttribute('data-kiezradars') && JSON.parse(el.getAttribute('data-kiezradars'))
const searchProfile = el.getAttribute('data-search-profile') && JSON.parse(el.getAttribute('data-search-profile'))
const selectedDistrict = el.getAttribute('data-selected-district')
const selectedTopic = el.getAttribute('data-selected-topic')
Expand Down Expand Up @@ -46,6 +47,7 @@ function init () {
districts={districts}
topicChoices={topicChoices}
participationChoices={participationChoices}
kiezradars={kiezradars}
searchProfile={searchProfile}
searchProfilesApiUrl={searchProfilesApiUrl}
searchProfilesCount={searchProfilesCount}
Expand Down
75 changes: 63 additions & 12 deletions meinberlin/react/projects/ProjectsControlBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,23 @@ const initialState = {
participations: [],
topics: [],
projectState: ['active', 'future'],
plansOnly: false
plansOnly: false,
kiezradar: null
}

const getAlteredFilters = ({ search, districts, topics, projectState, organisation, participations }, topicChoices, participationChoices) => {
const getAlteredFilters = (
{
search,
districts,
topics,
projectState,
organisation,
participations,
kiezradar
},
topicChoices,
participationChoices
) => {
const filters = []
if (search !== initialState.search) {
filters.push({ label: search, type: 'search', value: search })
Expand All @@ -63,6 +76,9 @@ const getAlteredFilters = ({ search, districts, topics, projectState, organisati
filters.push({ label: choice.name, type: 'participations', value: participationId })
}
})
if (kiezradar) {
filters.push({ label: kiezradar.name, type: 'kiezradar', value: kiezradar.id })
}

return filters
}
Expand All @@ -76,6 +92,7 @@ export const ProjectsControlBar = ({
onFiltered,
onResetClick,
hasContainer,
kiezradars,
searchProfile: initialSearchProfile,
searchProfilesApiUrl,
searchProfilesCount: initialSearchProfilesCount,
Expand All @@ -93,16 +110,10 @@ export const ProjectsControlBar = ({

const isFiltersInitialState = JSON.stringify(appliedFilters) === JSON.stringify(initialState)

const removeSearchProfile = () => {
setSearchProfile(null)
window.history.replaceState({}, '', window.location.pathname)
}

const createSearchProfile = (searchProfile) => {
setSearchProfile(searchProfile)
setSearchProfilesCount(searchProfilesCount + 1)
window.history.replaceState({}, '', window.location.pathname + '?search-profile=' + searchProfile.id
)
setParams([{ 'search-profile': searchProfile.id }])
}

return (
Expand All @@ -116,14 +127,35 @@ export const ProjectsControlBar = ({
newFilters.projectState = initialState.projectState
}
onFiltered(newFilters)
removeSearchProfile()
setSearchProfile(null)
setParams(
newFilters.kiezradar ? [{ kiezradar: newFilters.kiezradar.id }] : []
)
}}
>
<div className="facetingform__container">
<div className="facets">
<div className="container">
<fieldset className="facet">
<div className="facet__body">
<div className="form-group">
<div style={{ display: 'flex', gap: '5px' }}>
{kiezradars.map((kiezradar) => (
<button
key={kiezradar.id}
onClick={() => onFilterChange('kiezradar', kiezradar)}
style={{
backgroundColor: filters.kiezradar?.id === kiezradar.id ? 'red' : '#DDD',
padding: '10px',
borderRadius: '3px'
}}
type="button"
>
{kiezradar.name}
</button>
))}
</div>
</div>
<div className="searchform-slot mb-3">
<div className="form-group">
<label htmlFor="searchterm" className="form-label">
Expand Down Expand Up @@ -218,7 +250,7 @@ export const ProjectsControlBar = ({
aria-expanded={expandFilters}
type="button"
>
<span className={'fa fa-chevron-' + (expandFilters ? 'up' : 'down')} aria-hidden="true" />&nbsp;
<span className={'fa fa-chevron-' + (expandFilters ? 'up' : 'down')} aria-hidden="true" />&nbsp
{expandFilters ? translated.hideFilters : translated.showFilters}
</button>
</div>
Expand All @@ -230,8 +262,9 @@ export const ProjectsControlBar = ({
className="link"
onClick={() => {
setFilters(initialState)
setSearchProfile(null)
setParams([])
onResetClick()
removeSearchProfile()
}}
>
{translated.reset}
Expand Down Expand Up @@ -292,3 +325,21 @@ export const ProjectsControlBar = ({
</nav>
)
}

function setParams (params) {
if (params.length === 0) {
window.history.replaceState({}, '', window.location.pathname)
return
}

const searchParams = new URLSearchParams()

params.forEach(paramObj => {
Object.entries(paramObj).forEach(([key, value]) => {
searchParams.set(key, value)
})
})

const url = window.location.pathname + '?' + searchParams.toString()
window.history.replaceState({}, '', url)
}
6 changes: 4 additions & 2 deletions meinberlin/react/projects/ProjectsListMapBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const ProjectsListMapBox = ({
participationChoices,
projectStatus,
organisations,
kiezradars,
searchProfile,
searchProfilesCount,
isAuthenticated
Expand All @@ -59,7 +60,7 @@ const ProjectsListMapBox = ({
const [projectState, setProjectState] = useState(getDefaultProjectState(searchProfile))
const [items, setItems] = useState([])
const fetchCache = useRef({})
const [appliedFilters, setAppliedFilters] = useState(getDefaultState(searchProfile))
const [appliedFilters, setAppliedFilters] = useState(getDefaultState(searchProfile, kiezradars))

const fetchItems = useCallback(async () => {
setLoading(true)
Expand Down Expand Up @@ -125,9 +126,10 @@ const ProjectsListMapBox = ({
setAppliedFilters(filters)
}}
onResetClick={() => {
setAppliedFilters(getDefaultState(searchProfile))
setAppliedFilters(getDefaultState(searchProfile, kiezradars))
setProjectState(['active', 'future'])
}}
kiezradars={kiezradars}
searchProfile={searchProfile}
searchProfilesApiUrl={searchProfilesApiUrl}
searchProfilesCount={searchProfilesCount}
Expand Down
33 changes: 31 additions & 2 deletions meinberlin/react/projects/filter-projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,44 @@ export const isInTitle = (title, search) => {
const statusNames = ['active', 'future', 'past']

export const filterProjects = (items, appliedFilters, projectState) => {
const { search, topics, districts, organisation, participations, plansOnly } = appliedFilters
const { search, topics, districts, organisation, participations, plansOnly, kiezradar } = appliedFilters

return items.filter((item) => {
const isWithinRadius =
!kiezradar || haversineDistance(item.point.geometry.coordinates, kiezradar.point.geometry.coordinates) <= kiezradar.radius

return (topics.length === 0 || topics.some(topic => item.topics.includes(topic))) &&
(districts.length === 0 || districts.includes(item.district)) &&
(participations.length === 0 || participations.includes(item.participation)) &&
(organisation.length === 0 || organisation.includes(item.organisation)) &&
(search === '' || isInTitle(item.title, search)) &&
(projectState.includes(statusNames[item.status])) &&
(!plansOnly || item.type === 'plan')
(!plansOnly || item.type === 'plan') &&
(!kiezradar || isWithinRadius)
})
}

/**
* Calculates the shortest distance between two points on Earth's surface
* using the Haversine formula.
* https://en.wikipedia.org/wiki/Haversine_formula
* https://mapsplatform.google.com/resources/blog/how-calculate-distances-map-maps-javascript-api/
**/
const haversineDistance = (coords1, coords2) => {
const toRad = (angle) => (angle * Math.PI) / 180

const [lon1, lat1] = coords1
const [lon2, lat2] = coords2

const earthRadiusInMeters = 6371000
const deltaLat = toRad(lat2 - lat1)
const deltaLon = toRad(lon2 - lon1)

const haversineFormula =
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2)

const centralAngle = 2 * Math.atan2(Math.sqrt(haversineFormula), Math.sqrt(1 - haversineFormula))

return earthRadiusInMeters * centralAngle
}
38 changes: 24 additions & 14 deletions meinberlin/react/projects/getDefaultState.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,37 @@ const defaultState = {
organisation: [],
participations: [],
topics: [],
plansOnly: false
plansOnly: false,
kiezradar: null
}

export const getDefaultState = (searchProfile) => {
let mergeData = {}

export const getDefaultState = (searchProfile, kiezradars) => {
if (searchProfile) {
mergeData = {
search: searchProfile.query_text ?? '',
districts: searchProfile.districts.map(d => d.name),
organisation: searchProfile.organisations.map(o => o.name),
participations: searchProfile.project_types.map(p => p.id),
topics: searchProfile.topics.map((t) => t.code),
plansOnly: searchProfile.plans_only
return {
...defaultState,
...{
search: searchProfile.query_text ?? '',
districts: searchProfile.districts.map(d => d.name),
organisation: searchProfile.organisations.map(o => o.name),
participations: searchProfile.project_types.map(p => p.id),
topics: searchProfile.topics.map((t) => t.code),
plansOnly: searchProfile.plans_only,
kiezradar: searchProfile.kiezradar
}
}
}

return {
...defaultState,
...mergeData
const kiezradarId = new URLSearchParams(window.location.search).get('kiezradar')
const kiezradar = kiezradars.find((kiezradar) => kiezradar.id === parseInt(kiezradarId, 10))

if (kiezradar) {
return {
...defaultState,
...{ kiezradar }
}
}

return defaultState
}

export const getDefaultProjectState = (searchProfile) => {
Expand Down

0 comments on commit 2f158e8

Please sign in to comment.