from base64 import b64decode, b64encode
from markupsafe import Markup
from onegov.core.custom import json
from onegov.form.display import registry, BaseRenderer
from onegov.gis.forms.widgets import CoordinatesWidget
from onegov.gis.models import Coordinates
from wtforms.fields import StringField
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from onegov.form.types import (
FormT, PricingRules, RawFormValue, Validators)
from onegov.gis.models.coordinates import AnyCoordinates
from typing import Self
from wtforms.fields.core import _Filter, _Widget
from wtforms.form import BaseForm
from wtforms.meta import _SupportsGettextAndNgettext, DefaultMeta
[docs]
class CoordinatesField(StringField):
""" Represents a single pair of coordinates with optional zoom and
marker icon/color selection.
In the browser and during transit the point is stored as a base64 encoded
json string on a simple input field. For example::
eydsYXQnOiA4LjMwNTc2ODY5MTczODc5LCAnbG.. (and so on)
=>
{'lon': 8.30576869173879, 'lat': 47.05183585, 'zoom': 10}
For verification: This points to the Seantis office in Lucerne.
For convenience, the coordinates are accessible with the
:class:`onegov.gis.models.coordinates.Coordinates` class when 'data' is
used.
Note that this field doesn't work with the ``InputRequired`` validator.
Instead the ``DataRequired`` validator has to be chosen.
"""
[docs]
data: 'AnyCoordinates' # type:ignore[assignment]
def __init__(
self,
label: str | None = None,
validators: 'Validators[FormT, Self] | None' = None,
filters: 'Sequence[_Filter]' = (),
description: str = '',
id: str | None = None,
default: 'AnyCoordinates | Callable[[], AnyCoordinates] | None' = None,
widget: '_Widget[Self] | None' = None,
render_kw: dict[str, Any] | None = None,
name: str | None = None,
_form: 'BaseForm | None' = None,
_prefix: str = '',
_translations: '_SupportsGettextAndNgettext | None' = None,
_meta: 'DefaultMeta | None' = None,
# onegov specific kwargs that get popped off
*,
fieldset: str | None = None,
depends_on: 'Sequence[Any] | None' = None,
pricing: 'PricingRules | None' = None,
):
super().__init__(
label=label,
validators=validators,
filters=filters,
description=description,
id=id,
default=default, # type:ignore[arg-type]
widget=widget,
render_kw=render_kw,
name=name,
_form=_form,
_prefix=_prefix,
_translations=_translations,
_meta=_meta
)
self.data = getattr(self, 'data', Coordinates())
[docs]
def _value(self) -> str:
text = json.dumps(self.data) or '{}'
text_b = b64encode(text.encode('ascii'))
text = text_b.decode('ascii')
return text
[docs]
def process_data(self, value: object) -> None:
if isinstance(value, dict):
self.data = Coordinates(**value)
elif isinstance(value, Coordinates):
self.data = value
else:
self.data = Coordinates()
[docs]
def populate_obj(self, obj: object, name: str) -> None:
setattr(obj, name, self.data)
@registry.register_for('CoordinatesField')
[docs]
class CoordinatesFieldRenderer(BaseRenderer):
[docs]
def __call__(self, field: CoordinatesField) -> Markup: # type:ignore
return Markup("""
<div class="marker-map"
data-map-type="thumbnail"
data-lat="{lat}"
data-lon="{lon}"
data-zoom="{zoom}">
{lat}, {lon}
</div>
""").format(
lat=field.data.lat,
lon=field.data.lon,
zoom=field.data.zoom
)